Packages, __init__.py and Project Structure

A package is a directory containing Python modules, marked as a package with an __init__.py file. Packages let you organise a large codebase into logical namespaces โ€” app.routers, app.models, app.schemas โ€” so that related code lives together and can be imported cleanly. A well-structured FastAPI project is a package (or a set of packages) where the layout reflects the application architecture. Getting the project structure right from the start saves significant refactoring later and makes the codebase easy for new team members to navigate.

Creating a Package

# A package is a directory with __init__.py
# app/
#   __init__.py        โ† makes 'app' a package
#   main.py
#   config.py
#   database.py
#   routers/
#     __init__.py      โ† makes 'routers' a sub-package
#     posts.py
#     users.py
#     auth.py
#   models/
#     __init__.py
#     post.py
#     user.py
#     comment.py
#   schemas/
#     __init__.py
#     post.py
#     user.py
#   services/
#     __init__.py
#     post_service.py
#     email_service.py
#   utils/
#     __init__.py
#     security.py
#     pagination.py

# Imports from nested packages
from app.routers import posts
from app.models.post import Post
from app.schemas.user import UserCreate, UserResponse
from app.services.email_service import send_welcome_email
Note: In Python 3.3+, __init__.py is optional โ€” directories without it are treated as namespace packages. However, for FastAPI projects, always include __init__.py files. They make the package structure explicit, allow you to control what is exported from each package, and ensure compatibility with all tools (pytest, mypy, linters). An empty __init__.py file is perfectly fine โ€” you only need to add code to it when you want to simplify imports.
Tip: Use __init__.py to create convenient import shortcuts. If app/schemas/__init__.py contains from .post import PostCreate, PostResponse and from .user import UserCreate, UserResponse, then other modules can write from app.schemas import PostCreate, UserResponse instead of the longer from app.schemas.post import PostCreate. This pattern is called re-exporting and makes the public API of a package clear and concise.
Warning: As your FastAPI project grows, resist the temptation to put everything in a single large main.py or a flat collection of files. A flat structure becomes unmaintainable quickly โ€” 500-line files with mixed concerns (database models, request schemas, route handlers, and utilities all in one file) are extremely hard to test, review, and maintain. Structure your project by concern from day one: routers together, models together, schemas together.

__init__.py โ€” Package Initialisation and Re-exports

# app/schemas/__init__.py โ€” re-export for clean imports
from .post import PostCreate, PostUpdate, PostResponse
from .user import UserCreate, UserUpdate, UserResponse, UserLogin
from .comment import CommentCreate, CommentResponse

# Now external code can import from the package:
from app.schemas import PostCreate, UserResponse
# Instead of the verbose:
from app.schemas.post import PostCreate
from app.schemas.user import UserResponse

# app/__init__.py โ€” version information
__version__ = "1.0.0"
__author__  = "Your Name"

# app/models/__init__.py โ€” register all models so SQLAlchemy sees them
from .user    import User    # noqa: F401
from .post    import Post    # noqa: F401
from .comment import Comment # noqa: F401
# The noqa comment suppresses "unused import" lint warnings
# SQLAlchemy needs all models imported before create_all() is called
# Production FastAPI project structure
# blog-api/
#   app/
#     __init__.py
#     main.py           โ† FastAPI app creation, middleware, router inclusion
#     config.py         โ† Settings class (pydantic-settings)
#     database.py       โ† SQLAlchemy engine, session, Base
#     dependencies.py   โ† Common FastAPI Depends() functions
#     routers/
#       __init__.py
#       auth.py
#       posts.py
#       users.py
#       comments.py
#     models/           โ† SQLAlchemy ORM models
#       __init__.py
#       user.py
#       post.py
#       comment.py
#     schemas/          โ† Pydantic request/response models
#       __init__.py
#       user.py
#       post.py
#       comment.py
#     services/         โ† Business logic (called by routers)
#       __init__.py
#       auth_service.py
#       post_service.py
#       email_service.py
#     utils/
#       __init__.py
#       security.py     โ† password hashing, JWT
#       pagination.py
#       email.py
#   tests/
#     conftest.py
#     test_auth.py
#     test_posts.py
#   alembic/            โ† database migrations
#   .env
#   requirements.txt
#   requirements-dev.txt
#   pyproject.toml

app/main.py โ€” The Entry Point

# app/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from app.config import settings
from app.routers import auth, posts, users, comments

app = FastAPI(
    title       = settings.app_name,
    description = "A full-featured blog API built with FastAPI",
    version     = "1.0.0",
    docs_url    = "/docs",      # Swagger UI
    redoc_url   = "/redoc",     # ReDoc
)

# โ”€โ”€ Middleware โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
app.add_middleware(
    CORSMiddleware,
    allow_origins     = settings.allowed_origins,
    allow_credentials = True,
    allow_methods     = ["*"],
    allow_headers     = ["*"],
)

# โ”€โ”€ Routers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
app.include_router(auth.router,     prefix="/api/auth",     tags=["auth"])
app.include_router(posts.router,    prefix="/api/posts",    tags=["posts"])
app.include_router(users.router,    prefix="/api/users",    tags=["users"])
app.include_router(comments.router, prefix="/api/comments", tags=["comments"])

@app.get("/api/health", tags=["health"])
def health_check():
    return {"status": "ok", "version": "1.0.0"}

Common Mistakes

Mistake 1 โ€” Flat structure that does not scale

โŒ Wrong โ€” all code in one file:

# main.py โ€” 1200 lines with models, schemas, routes, and utils all mixed
from sqlalchemy import Column, Integer, String
from pydantic import BaseModel
# ... everything in one file

โœ… Correct โ€” separate concerns into packages from the start.

Mistake 2 โ€” Missing __init__.py causing import failures

โŒ Wrong โ€” directory without __init__.py treated as namespace package:

# app/routers/ has no __init__.py
from app.routers.posts import router   # may fail in some tool configurations

โœ… Correct โ€” always include __init__.py in every package directory.

Mistake 3 โ€” Importing models in wrong order causing SQLAlchemy issues

โŒ Wrong โ€” relationships undefined because model not imported yet:

# database.py calls Base.metadata.create_all(engine)
# But User model never imported โ†’ table not created!

โœ… Correct โ€” import all models before create_all():

from app.models import User, Post, Comment   # โœ“ all imported
Base.metadata.create_all(engine)

Quick Reference

Concept Detail
Package marker __init__.py in directory
Re-export from package from .module import Name in __init__.py
Package version __version__ = "1.0.0" in __init__.py
Import from sub-package from app.routers.posts import router
Include router app.include_router(router, prefix="/api/posts")

🧠 Test Yourself

You have app/schemas/post.py with PostCreate and PostResponse. What code in app/schemas/__init__.py lets other modules use from app.schemas import PostCreate instead of from app.schemas.post import PostCreate?