A clean project layout saves you hours of debugging and makes collaboration possible.
When I started writing Python scripts, everything lived in one folder. One giant file. No structure. It worked fine until the project grew beyond a few hundred lines. Then everything became hard to find, hard to test, and hard for anyone else to understand.
Both of my open source projects (QuietKeep and QuietLedger) follow a consistent layout that has made development, testing, and packaging much easier. This is the pattern I use and why each piece matters.
The Layout
my-project/
README.md
LICENSE
requirements.txt
.gitignore
.env.example
src/
app.py
config.py
models/
__init__.py
user.py
database.py
routes/
__init__.py
api.py
auth.py
utils/
__init__.py
helpers.py
crypto.py
tests/
test_models.py
test_routes.py
docs/
USER_GUIDE.md
Not every project needs all of this. A small script might just need the README, requirements file, and a single source file. But as soon as a project has more than one module, this structure pays for itself.
What Each Piece Does
README.md First thing anyone sees. What the project does, how to install it, how to run it.
LICENSE Tells people what they can and cannot do with your code. Pick one early.
requirements.txt Every dependency your project needs, pinned to specific versions. Anyone can recreate your environment with pip install -r requirements.txt.
.gitignore Keeps junk out of your repository. Python bytecode, virtual environment folders, .env files with secrets, IDE configs.
.env.example Shows what environment variables are needed without exposing real values. Someone cloning the repo copies this to .env and fills in their own credentials.
src/ All application code lives here. Not in the root. This keeps the top level clean and makes imports predictable.
tests/ Separate from source code. Test files mirror the source structure so you always know where to find the test for a given module.
docs/ Anything longer than a README section goes here. User guides, API docs, architecture notes.
Virtual Environments
Never install project dependencies globally. Always use a virtual environment:
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
This isolates your project’s packages from the system Python and from other projects. Each project gets its own environment with its own versions. No conflicts.
Add venv/ to your .gitignore. The virtual environment folder should never be committed. The requirements.txt file is the source of truth for dependencies.
Pinning Dependencies
Always pin versions in requirements.txt:
flask==3.1.0
sqlalchemy==2.0.35
python-dotenv==1.0.1
If you just write flask without a version, you’ll get whatever the latest is at install time. That means your project might work today and break tomorrow when a dependency pushes a breaking change. Pinning means reproducible builds.
Generate your current pinned list with:
pip freeze > requirements.txt
The .gitignore
At minimum for a Python project:
venv/
__pycache__/
*.pyc
.env
*.db
.DS_Store
This keeps bytecode, secrets, local databases, and OS junk out of your repo.
When to Add Structure
- One file, under 200 lines? Keep it simple. One script, one README, one requirements file.
- Multiple modules or features? Move to the
src/layout. Split by responsibility. - Other people will use or contribute? Add docs, tests, license, and a clear README from the start.
The goal is not to over-engineer from day one. The goal is to have a clear path for growth. When you need to add a feature or fix a bug six months from now, you should be able to find the right file in seconds.
Commenting Your Code
A clean folder structure tells people where to find things. Comments tell them what the code is actually doing and why. The two work together.
I have a separate post on this: How to Actually Comment Code Without Wasting Everyone’s Time. Worth reading alongside this one.
Links
Your Turn
This is the structure that works for me. It is not the only way to do it.
If you use a different layout, have a reason for breaking from this pattern, or think something here is wrong, say so. That kind of pushback is useful for everyone reading. If you are newer to Python and this helped you, I would like to hear what clicked for you too.
Drop your thoughts below.