A module is any Python file — when you write import math, Python finds math.py (or a compiled equivalent) and makes its names available. When your FastAPI project grows beyond a single file, you split it into multiple modules: one for route handlers, one for database models, one for Pydantic schemas. Understanding how Python resolves module names, the different import syntaxes, and how to avoid circular imports is essential for structuring any production Python application.
Basic Import Syntax
# ── import the whole module ────────────────────────────────────────────────────
import math
import os
import datetime
print(math.pi) # 3.141592653589793
print(math.sqrt(16)) # 4.0
print(os.getcwd()) # /home/user/project
# ── from...import — import specific names ─────────────────────────────────────
from math import pi, sqrt, ceil
from os import getcwd, path
from datetime import datetime, timedelta
print(pi) # 3.141592653589793 — no math. prefix
print(sqrt(25)) # 5.0
print(ceil(3.2)) # 4
# ── import with alias — shorten long module names ─────────────────────────────
import datetime as dt
import os.path as osp
from collections import defaultdict as ddict
now = dt.datetime.now()
exists = osp.exists("/tmp/test.txt")
# ── from...import * — import all public names (avoid in production) ───────────
from math import * # imports ALL names from math module
# Pollutes the namespace — hard to tell where names came from
# Never do this in FastAPI application code
PYTHONPATH environment variable, (3) the standard library directories, (4) site-packages (third-party packages installed with pip). This search path is stored in sys.path — you can print it with import sys; print(sys.path). Understanding this order explains why naming a local file os.py or json.py will shadow the standard library module of the same name.from module import name over import module when you use a name frequently — it reduces repetition and makes code more readable. Prefer import module when you use the module name to make the source of a function clear, or when two modules export the same name. In FastAPI projects, the convention is from fastapi import FastAPI, HTTPException, Depends rather than import fastapi.datetime.py, json.py, os.py, or email.py in your project directory will shadow the real module — your code will import your file instead of the standard library module, causing confusing ImportError or AttributeError messages. This is a surprisingly common error in beginner FastAPI projects.Importing from Your Own Modules
# Project structure:
# myproject/
# main.py
# utils.py
# models.py
# utils.py
def format_date(dt):
return dt.strftime("%Y-%m-%d")
MAX_PAGE_SIZE = 100
# models.py
class Post:
def __init__(self, title, body):
self.title = title
self.body = body
# main.py — importing from sibling modules
from utils import format_date, MAX_PAGE_SIZE
from models import Post
post = Post("Hello", "World")
print(format_date(post.created_at))
print(f"Max page size: {MAX_PAGE_SIZE}")
Relative Imports — Within a Package
# Within a package, use relative imports (dot notation)
# app/
# __init__.py
# main.py
# routers/
# __init__.py
# posts.py ← current file
# users.py
# models/
# __init__.py
# post.py
# In app/routers/posts.py:
# Absolute import — works from anywhere
from app.models.post import Post
from app.config import settings
# Relative import — relative to current package location
from ..models.post import Post # go up one level to app/, then models/post
from . import users # import sibling module users.py
from .users import get_current_user # import name from sibling module
# FastAPI convention: use absolute imports — clearer and IDE-friendly
# from app.models.post import Post ← preferred in FastAPI projects
The if __name__ == “__main__” Guard
# Every Python file has a __name__ attribute
# When run directly: __name__ == "__main__"
# When imported: __name__ == the module name
# utils.py
def greet(name):
return f"Hello, {name}!"
# This block only runs when utils.py is executed directly:
# python3 utils.py
# It does NOT run when 'import utils' is used in another file
if __name__ == "__main__":
print(greet("World")) # only runs on direct execution
# FastAPI app — typical pattern in main.py
import uvicorn
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def root():
return {"message": "Hello"}
if __name__ == "__main__":
# Only runs when: python3 main.py
# Not when: uvicorn main:app --reload
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
Avoiding Circular Imports
# Circular import — A imports B which imports A
# models.py imports from schemas.py
# schemas.py imports from models.py
# → ImportError: cannot import name 'Post' from partially initialized module
# ── Solution 1: Restructure to break the cycle ────────────────────────────────
# Move shared code to a third module (e.g. base.py) that neither imports the other
# ── Solution 2: Import inside the function (lazy import) ─────────────────────
# models.py
def get_schema():
from schemas import PostSchema # imported here, not at module level
return PostSchema
# ── Solution 3: Use TYPE_CHECKING guard ───────────────────────────────────────
from typing import TYPE_CHECKING
if TYPE_CHECKING:
# This block only runs for type checkers (mypy), not at runtime
from schemas import PostSchema # safe — never executed at import time
def process(schema: "PostSchema") -> None: # use string annotation
pass
Common Mistakes
Mistake 1 — Shadowing a standard library module
❌ Wrong — file named after a standard library module:
# File: datetime.py (in project root)
# Another file: main.py
from datetime import datetime # imports YOUR datetime.py, not the stdlib!
# AttributeError: module 'datetime' has no attribute 'datetime'
✅ Correct — use descriptive project-specific names:
# Rename to: date_utils.py, time_helpers.py, etc. ✓
Mistake 2 — from module import * polluting namespace
❌ Wrong — unclear where names come from:
from os import *
from sys import *
# Is 'path' from os or sys? Impossible to tell
✅ Correct — explicit imports:
from os import path, getcwd
from sys import argv, exit # ✓ clear origin
Mistake 3 — Circular imports from top-level module-level code
❌ Wrong — importing at module level creates a circle:
# a.py: from b import thing
# b.py: from a import other_thing
# → ImportError on startup
✅ Correct — restructure, use lazy imports inside functions, or use TYPE_CHECKING.
Quick Reference
| Pattern | Code |
|---|---|
| Import module | import math |
| Import names | from math import pi, sqrt |
| Alias module | import numpy as np |
| Alias name | from datetime import datetime as dt |
| Relative import | from ..models import Post |
| Run guard | if __name__ == "__main__": |
| Lazy import | def f(): from module import X |
| Type-only import | if TYPE_CHECKING: from module import X |