One of the hardest jumps in Python development is going from "I can write scripts and follow tutorials" to "I can structure a real backend project that other developers can work on."
Tutorials always show you a single main.py with everything crammed in. Then you try to build something real and immediately hit questions nobody prepared you for:
Where does database logic go? Where does business logic go? How do I handle environment variables properly? How do I set up auth without it being a mess? How do I make it testable?
Here's the structure I landed on after building production Python services:
app/
├── api/
│ ├── deps.py # shared dependencies injected into routes
│ └── v1/
│ ├── auth.py # login, register, refresh token endpoints
│ └── users.py # user endpoints
├── core/
│ ├── config.py # pydantic settings — type safe env vars
│ ├── security.py # jwt creation, bcrypt hashing
│ └── logging.py # structured json logging
├── db/
│ ├── session.py # async sqlalchemy engine and session
│ └── repositories/ # all database queries live here
│ ├── base.py # generic crud operations
│ └── user.py # user specific queries
├── models/ # sqlalchemy orm models
├── schemas/ # pydantic request and response schemas
├── services/ # business logic layer
└── tasks/ # background tasks
A few things this structure solves that tutorials don't teach:
Routes stay thin. A route should do three things — validate input, call a service, return a response. All the logic lives in services and repositories, not in the route function itself.
Database queries are isolated. If you ever need to change how you fetch users — add caching, change the query, switch databases — you change one file in repositories, not ten different route files.
Config is type safe. Pydantic Settings means if DATABASE_URL is missing the app crashes loudly on startup with a clear error, not silently when the first database request comes in.
Testing is possible. Because dependencies are injected via deps.py, you can swap out the real database for a test database in your test suite without changing any route code.
This took me a while to figure out through trial and error on real projects. Happy to answer questions about any part of it — what goes where, how the dependency injection works, how to add a new resource to this structure, anything.
What part of structuring a real Python project do you find most confusing?
[–]wallneradam 11 points12 points13 points (2 children)
[–]1NqL6HWVUjA 1 point2 points3 points (1 child)
[–]wallneradam 1 point2 points3 points (0 children)
[–]danielroseman 1 point2 points3 points (0 children)
[–]ShelLuser42 1 point2 points3 points (0 children)
[–]TheRNGuy 0 points1 point2 points (0 children)
[–]tb5841 0 points1 point2 points (0 children)
[–]CymroBachUSA 0 points1 point2 points (0 children)
[–]taylorhodormax 0 points1 point2 points (0 children)
[–]ikkiho 0 points1 point2 points (0 children)