Structuring Python Projects the Right Way (with FastAPI Example)

While building a mini-project using FastAPI, along with SQLAlchemy and Pydantic, I realized something important: as the project grows, it becomes essential to organize your Python files into a clear directory structure. You can group related Python files (modules) inside directories to keep the codebase clean, modular, and easier to maintain. This not only improves readability but also makes debugging and collaboration much simpler.
Many tutorials might show you how to structure a project, but they rarely explain why it’s important. Understanding project structure is one of those foundational Python concepts that everyone should know before diving into complex projects.
Knowing how to organize your project will save you from hours of frustration and countless "ModuleNotFoundError"
issues. Personally, I hit this error so many times, even in a small project, that I paused learning FastAPI and decided to understand project structuring first.
It also helped me understand key Python concepts like absolute imports and relative imports, which are essential when your codebase grows.
Example project structure:
jobs_tracker/ /* ROOT REPO */
|------ app/
|------ main.py /* Entry point of the application */
|------ setup_db.py /* DB setup code */
|------ jobs_schema.py /* Pydantic schema of input */
|------ jobs.db /* SQLite database file */
|------ db/
|------ __init__.py
|------ crud.py /* CRUD operations */
|------ dbconnect.py /* DB connection & session */
|------ tables.py /* SQLAlchemy table definitions */
|------ test/
|------ basic_test1.py /* Basic test scripts */
Rules to keep in mind while structuring your project:
Running Files & Import Resolution: When you run a Python file, Python uses the directory you ran the file from as the root for resolving imports.
In this case,jobs_tracker/
is your root, and you’ll typically run the app like this:uvicorn app.main:app --reload # way to run fastapi applications
Where to Place Files: If you have only one file like
jobs_schema.py,
it can stay in/app
. No need for a separate directory. But if your schemas grow into multiple modules, consider placing them in a newschemas/
folder insideapp/
.Importing modules correctly:
Here’s how Python handles imports:
import modu
This looks for
modu.py
in the same directory as the caller. If not found, Python searches through the directories listed insys.path
, and throws anImportError
if it’s still missing.According to The Hitchhiker’s Guide to python, here are the three main ways to import modules, and which is best:
from modu import *
: Avoid this. It’s unclear what you're importing and may overwrite existing names.from modu import sqrt
: Better, but can cause conflicts ifsqrt
exists in bothmodu
and the caller module.import modu ...... modu.sqrt(45)
: This is the cleanest way. It makes it clear where each method comes from and avoids name clashes.
What Is
__init__.py
and Why Do We Use It?Any directory that contains an
__init__.py
file is considered a Python package. This file tells Python to treat the directory as a package, which allows you to import files (modules) from it using dot notation.Even if the file is empty, it's necessary for consistent import behaviour (especially in Python <3.3 or complex setups). Think of it as Python's way of saying:
"Hey, this folder contains Python code that should be treated as a unit."Example:
pack/ ├── __init__.py ├── modu.py └── main.py
To use
modu.py
inmain.py
:import pack.modu result = pack.modu.sqrt(67)
This makes it easy to organize reusable logic in different folders, and still keep your imports clean and traceable.
Absolute Imports vs Relative Imports
Understanding the difference between absolute imports and relative imports is key to organizing larger Python projects cleanly.
Absolute Imports
When you import a module using its full path from the project root, it’s called an absolute import.
Example 1: Importing crud.py
in main.py
main.py
is inside app/
, and crud.py
is inside app/db/
# app/main.py
from app.db import crud
# or
from app.db.crud import add_jobs
Example 2: Importing jobs_schema.py
in crud.py
crud.py
is inside app/db/
, and jobs_schema.py
is in app/
# app/db/crud.py
from app import jobs_schema
# or
from app.jobs_schema import JobsStructure
Use absolute imports when:
You are importing across packages.
You want clearer, consistent code.
You are running your script from the project root (which is the default in production apps).
Relative Imports
Relative imports are based on the current file’s location within the package. You use .
for the current directory and ..
to move up one level.
Example: Importing tables.py
and dbconnect.py
in crud.py
# crud.py
from . import dbconnect
from . import tables
Common Pitfall: Running a File with Relative Imports
Let’s say you want to run tables.py
directly for testing or schema generation:
# app/db/tables.py
from . import dbconnect # This breaks when run directly
You’ll get this error: ImportError: attempted relative import with no known parent package.
This happens because Python treats any directly run script as __main__
and not as part of a package, so it cannot resolve relative imports.
Solution?
→ Option 1: Convert Relative Import to Absolute
Change this:
# app/db/tables.py
from . import dbconnect
To this:
from app.db import dbconnect
And then run it like this:
python -m app.db.tables
Using -m
runs the file as a module from the project root, so Python knows it's part of a package.
→ Option 2: Don't Run Internal Modules Directly
Instead, create a separate script (e.g., setup_db.py
) in the root folder that calls the internal module logic.
# app/setup_db.py
from app.db.tables import JobsApplied
from app.db.dbconnect import engine, Base
Base.metadata.create_all(engine)
Then run:
python app/setup_db.py
This keeps your internal modules clean and avoids breaking relative imports.
Common Errors you may find
ModuleNotFoundError
and RelativeImportError
Error | Cause | Fix |
ModuleNotFoundError | Python can't find the module in sys.path | Use correct absolute path, or check that your file structure and root are set up correctly. |
ImportError: attempted relative import with no known parent package | You're using a relative import, but running the file directly (like running tables.py directly) | Use -m to run as a module, or use absolute imports instead. |
Looks little confusing, right? Here is the table summarizing all the points mentioned above:
Use Case | Import Type | Example | Notes |
Importing from a different folder/package | Absolute | from app.db import crud | Preferred in real apps |
Importing between files in the same folder | Relative | from . import tables | Works only when run as part of the package (and not as an independent script) |
Running an internal file that uses relative imports | Run as a module | python -m app.db.tables | Keeps relative import working |
Want to run internal file directly | Use absolute import OR wrap in main script | from app.db import tables | Avoid running internal modules directly when possible |
I hope this helped you in some way. I'm still new to all of this and learning as I go — so if you spot anything incorrect, feel free to share your feedback. It means a lot!
Subscribe to my newsletter
Read articles from Harshita Sharma directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Harshita Sharma
Harshita Sharma
Learning to build AI applications.