How to Structure a Small Python Project Like a Professional
I inherited a project that was one 2,000-line file called main.py. Database queries, business logic, API handlers, utility functions—all jumbled together. Finding anything required Ctrl+F and prayer.
We spent a full week just splitting it into modules before we could safely make changes. A proper structure from day one would have taken 10 minutes to set up.
The Standard Layout
Here's what a well-organized Python project looks like:
my-project/
├── src/
│ └── myapp/
│ ├── __init__.py
│ ├── main.py
│ ├── utils.py
│ └── database.py
├── tests/
│ ├── __init__.py
│ └── test_utils.py
├── .gitignore
├── README.md
├── requirements.txt
└── .env
Let me explain why each piece matters.
Why the src/ Folder?
You could put myapp/ at the root. Many tutorials do this. But the "src layout" prevents a subtle bug.
Without src/, when you run tests from the project root, Python might import your local code instead of the installed package. Everything works on your machine, then breaks when deployed because the import paths are different.
The src/ folder forces you to install your package (even in development with pip install -e .), ensuring imports work the same way everywhere.
What's __init__.py For?
An empty __init__.py file tells Python "this folder is a package." Without it, you can't do from myapp import utils.
You can also use it to expose a cleaner API:
# src/myapp/__init__.py
from .utils import format_date, parse_config
# Now users can write:
# from myapp import format_date
# Instead of:
# from myapp.utils import format_date
The Entry Point
main.py is where your application starts. The key pattern:
# src/myapp/main.py
from myapp.database import connect
from myapp.utils import setup_logging
def main():
setup_logging()
db = connect()
print("App started")
# Your logic here
if __name__ == "__main__":
main()
The if __name__ == "__main__" check is crucial. Without it, if someone imports your main module to test a function, the entire app starts running. The check ensures code only runs when executed directly.
Virtual Environments
Never install packages globally. Create an isolated environment per project:
# Create
python -m venv venv
# Activate (Mac/Linux)
source venv/bin/activate
# Activate (Windows)
venv\Scripts\activate
# Install packages
pip install requests flask
# Save dependencies
pip freeze > requirements.txt
Now anyone can recreate your exact setup with pip install -r requirements.txt.
Add venv/ to .gitignore. Never commit it—it's not portable across machines.
Configuration and Secrets
This is important: never hardcode API keys or passwords in your code. If you commit them to git, they're compromised forever (git history is permanent).
Use environment variables instead:
# .env file (add to .gitignore!)
DATABASE_URL=postgres://user:pass@localhost/db
API_KEY=sk-secret-key-here
# src/myapp/config.py
import os
from dotenv import load_dotenv
load_dotenv()
DATABASE_URL = os.getenv("DATABASE_URL")
API_KEY = os.getenv("API_KEY")
if not API_KEY:
raise ValueError("API_KEY not set in environment")
Your code is public (on GitHub, in logs). Your .env file stays private on each machine.
The .gitignore
At minimum:
# .gitignore
venv/
__pycache__/
*.pyc
.env
*.egg-info/
dist/
build/
Quick Start Template
When starting a new project, I run:
mkdir my-project && cd my-project
mkdir -p src/myapp tests
touch src/myapp/__init__.py src/myapp/main.py
touch tests/__init__.py
python -m venv venv
source venv/bin/activate
echo "venv/\n__pycache__/\n.env" > .gitignore
echo "# My Project" > README.md
Takes 30 seconds. Saves hours of refactoring later.
When to Split Files
My rule: when a file hits 300 lines, consider splitting. When it hits 500, definitely split.
Group by responsibility: all database stuff in database.py, all API calls in api.py, all utility functions in utils.py. Future you (and your teammates) will thank you.