Modules and Imports — Organising Python Code

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
Note: Python searches for modules in this order: (1) the current directory / the directory of the running script, (2) directories listed in the 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.
Tip: Prefer 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.
Warning: Never name your own files the same as a standard library or third-party module you need to import. A file named 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

🧠 Test Yourself

You create a file called json.py in your FastAPI project’s root directory. Later, you write import json in another file and try to call json.loads('{"key": "value"}'). What happens?