Python Project Best Practices: Structure, venv, pyproject.toml, Testing, Ruff, Logging, Config, and Git
Quick Answer
A good Python project should be easy to run, test, maintain, and hand over.
Use this baseline for most serious projects:
my-python-project/
README.md
pyproject.toml
.gitignore
.env.example
src/
my_project/
__init__.py
config.py
main.py
tests/
test_main.py
scripts/
docs/
.venv/Commit code, tests, docs, and configuration examples. Do not commit .venv, .env, caches, logs, secrets, or large generated files.
Last verified: June 26, 2026.
Official Links
- Python virtual environments: https://docs.python.org/3/tutorial/venv.html
- Python packaging guide: https://packaging.python.org/
- PyPA
pyproject.tomlguide: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/ - PyPA
srclayout discussion: https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/ - pytest good practices: https://docs.pytest.org/en/stable/explanation/goodpractices.html
- Ruff docs: https://docs.astral.sh/ruff/
- pre-commit docs: https://pre-commit.com/
What You Will Learn
By the end, you should be able to:
- choose a practical Python project structure
- decide when to use
src/ - create an isolated environment
- use
requirements.txt,pyproject.toml, oruv - write a useful
.gitignore - create a minimal test suite
- run Ruff for linting and formatting
- manage secrets safely
- add logging instead of print debugging
- prepare a project for GitHub, CI, and deployment
1. What Makes A Python Project Good?
A good Python project has these properties:
- clear entry point
- predictable folders
- isolated dependencies
- documented setup steps
- repeatable test command
- no committed secrets
- readable configuration
- useful logs
- small commits
- easy onboarding for another developer
Best practices are not about making every beginner project enterprise-grade. They are about preventing predictable pain.
2. Start With The Project Type
Different projects need different structure.
| Project Type | Best Starting Point |
|---|---|
| one-file practice script | one .py file is fine |
| learning notebook | notebooks/, data/, README.md |
| data analysis project | notebooks/, src/, data/raw, data/processed |
| CLI tool | src/package_name/, pyproject.toml, tests/ |
| web app | src/ or app package, config module, tests, .env.example |
| package for reuse | src/ layout, pyproject.toml, tests, CI |
Do not over-structure a tiny script. Do not under-structure a project that has tests, data, config, and multiple modules.
3. Recommended Beginner Structure
Use this for learning projects:
learning-project/
README.md
requirements.txt
.gitignore
src/
analysis.py
data/
sample.csv
tests/
test_analysis.py
.venv/This keeps code, data, tests, and environment separate.
For a deeper walkthrough, see python-project-folder-structure-guide.
4. Recommended Professional Structure
Use this for apps, CLIs, and reusable code:
task-tracker/
README.md
pyproject.toml
.gitignore
.env.example
src/
task_tracker/
__init__.py
main.py
config.py
models.py
services.py
tests/
test_services.py
test_config.py
scripts/
seed_data.py
docs/
decisions.mdThe package code lives under src/task_tracker/.
The src/ layout helps catch import mistakes because your package is not accidentally imported from the repository root.
5. Use One Virtual Environment Per Project
Create .venv inside the project:
python -m venv .venvActivate it:
source .venv/bin/activateWindows PowerShell:
py -m venv .venv
.\.venv\Scripts\Activate.ps1Install packages only inside the environment.
Never commit .venv.
See python-virtual-environments-venv-requirements-guide for the full workflow.
6. Choose Dependency Management Deliberately
For beginner projects, requirements.txt is acceptable:
pandas==2.3.0
requests==2.32.0
pytest==8.4.0Install:
python -m pip install -r requirements.txtFor modern app or package projects, prefer pyproject.toml.
[project]
name = "task-tracker"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"pydantic>=2.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"ruff>=0.5",
]If you use uv, commit pyproject.toml and uv.lock.
See uv-python-package-manager-guide for a beginner-friendly uv workflow.
7. Use .gitignore From Day One
Minimum .gitignore:
.venv/
__pycache__/
*.pyc
.pytest_cache/
.ruff_cache/
.mypy_cache/
.env
*.log
.DS_Store
.ipynb_checkpoints/Commit .gitignore.
Do not commit secret files, local virtual environments, generated caches, or machine-specific settings.
8. Add .env.example, Not .env
Use .env locally:
DATABASE_URL=sqlite:///local.db
API_KEY=replace-with-local-secretDo not commit it.
Commit .env.example:
DATABASE_URL=sqlite:///example.db
API_KEY=
LOG_LEVEL=INFOThis tells other developers which variables are required without leaking secrets.
9. Write A Useful README
Minimum useful README:
Practice Lab
Task Tracker
Goal
Track tasks from the command line.
Setup
python -m venv .venv source .venv/bin/activate python -m pip install -e ".[dev]"
Run
python -m task_tracker
Test
pytest
A good README answers:
- What does this project do?
- How do I install it?
- How do I run it?
- How do I test it?
- Which environment variables are needed?
## 10. Keep Code Importable
Avoid putting all code in one script.
Weak structure:
```text
main.py
utils.py
helpers.pyBetter structure:
src/
task_tracker/
__init__.py
main.py
services.py
storage.pyRun modules with:
python -m task_tracker.mainImportable code is easier to test.
11. Separate Pure Logic From I/O
Pure logic is easier to test.
Weak version:
def calculate_total():
price = float(input("Price: "))
tax = price * 0.18
print(price + tax)Better:
def total_with_tax(price: float, tax_rate: float = 0.18) -> float:
return price + (price * tax_rate)Then put input/output in another function.
This makes tests simple:
def test_total_with_tax():
assert total_with_tax(100, 0.18) == 11812. Add Tests Early
Use pytest.
python -m pip install pytestRecommended layout:
tests/
test_services.py
test_config.pyTest files should start with test_.
Test functions should start with test_.
Run:
pytestAt minimum, test:
- important calculations
- parsing logic
- validation logic
- error handling
- file or API boundary behavior with mocks
13. Use Ruff For Linting And Formatting
Ruff can lint and format Python code.
Install:
python -m pip install ruffRun checks:
ruff check .Format:
ruff format .Example pyproject.toml config:
[tool.ruff]
line-length = 88
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]Keep tool configuration in pyproject.toml when possible.
14. Use Type Hints Where They Add Clarity
Type hints help readers and tools.
def slugify(title: str) -> str:
return title.lower().replace(" ", "-")Do not obsess over typing every beginner script.
Use type hints for:
- public functions
- service functions
- data models
- functions with tricky inputs
- functions used by tests
15. Use Logging Instead Of Print Debugging
For real projects, use logging:
import logging
logger = logging.getLogger(__name__)
def import_file(path: str) -> None:
logger.info("Importing file: %s", path)Configure logging once near the app entry point:
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)Use:
debugfor developer detailsinfofor normal lifecycle eventswarningfor recoverable problemserrorfor failed operationsexceptioninside exception handlers
16. Centralize Configuration
Avoid scattering environment lookups everywhere.
Weak:
import os
db_url = os.environ["DATABASE_URL"]Better:
from dataclasses import dataclass
import os
@dataclass(frozen=True)
class Settings:
database_url: str
log_level: str = "INFO"
def load_settings() -> Settings:
return Settings(
database_url=os.environ.get("DATABASE_URL", "sqlite:///local.db"),
log_level=os.environ.get("LOG_LEVEL", "INFO"),
)For larger projects, use a settings library such as Pydantic Settings.
17. Keep Secrets Out Of Code
Never hard-code:
- API keys
- database passwords
- OAuth secrets
- private tokens
- encryption keys
Bad:
API_KEY = "hardcoded-placeholder"Better:
import os
API_KEY = os.environ["API_KEY"]Also avoid printing secrets in logs.
18. Use Small Scripts For Operations
Keep repeatable helper scripts in scripts/.
scripts/
seed_data.py
export_report.py
migrate_local.pyScripts should be safe, documented, and idempotent when possible.
Avoid keeping one-off scratch files forever.
19. Write Small Commits
Good commit examples:
Add project config loader
Add pytest coverage for CSV parser
Fix missing value handling in importerAvoid commits like:
changes
final
misc fixesSmall commits make rollback and review easier.
20. Add Pre-Commit Hooks When The Project Grows
Pre-commit runs checks before commits.
Install:
python -m pip install pre-commitExample .pre-commit-config.yaml:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.0
hooks:
- id: ruff
- id: ruff-formatInstall hooks:
pre-commit installUse hooks to catch simple issues before CI.
21. Add CI For Important Projects
For GitHub Actions, a basic Python check can run:
- install dependencies
- run lint
- run tests
Example:
name: Python checks
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: python -m pip install -e ".[dev]"
- run: ruff check .
- run: ruff format --check .
- run: pytestCI is especially useful when other people depend on the project.
22. Keep Data And Outputs Under Control
For data projects:
data/
raw/
processed/
outputs/
charts/
reports/Rules:
- preserve raw data
- document how processed data is generated
- do not commit private data
- do not commit huge generated files
- commit small synthetic sample data when helpful
23. Document Decisions
For serious projects, add a lightweight decision log.
docs/
decisions.mdExample:
Decisions
2026-06-26: Use src layout
We use src layout so tests import the installed package instead of accidentally importing from the repo root.
This helps future maintainers understand why choices were made.
## 24. Project Checklist
Before sharing a project, check:
- `README.md` explains setup, run, and test commands
- `.gitignore` excludes `.venv`, `.env`, caches, logs
- `.env.example` documents required settings
- dependencies are reproducible
- code lives in a predictable folder
- tests run with one command
- Ruff or another linter runs cleanly
- secrets are not committed
- sample data is safe to publish
- logs do not expose secrets
- CI exists for important projects
## 25. Common Beginner Mistakes
### Committing `.venv`
Do not commit virtual environments. Recreate them from dependency files.
### Starting With Too Many Tools
You do not need Docker, pre-commit, mypy, CI, and packaging for a two-file practice script. Add tools when the project needs them.
### Keeping All Code In A Notebook
Notebooks are good for exploration. Move reusable logic into `.py` files.
### No Tests
Even three tests are better than none when the code will be reused.
### Hard-Coded Paths
Avoid machine-specific paths like:
```python
path = "C:/Users/name/Desktop/project/data.csv"Explanation
- This code assigns a string literal containing a file path to a variable named 'path'
- The path specifies the location of a CSV file on a Windows filesystem at C:/Users/name/Desktop/project/data.csv
- This approach centralizes file location information in one place, making it easy to modify the path later without searching through the entire codebase
- The variable can be reused throughout the program when referencing this specific data file
- Using forward slashes in the path works on Windows systems due to Python's path handling capabilities
Use relative paths or configuration.
Secret Values In Git
Once a secret is committed, assume it is compromised. Rotate it.
26. FAQ
What is the best Python project structure?
For serious projects, use README.md, pyproject.toml, .gitignore, .env.example, src/, tests/, and docs/. For small learning scripts, a simpler layout is fine.
Should I use src layout in Python?
Use src layout for packages, apps, and projects with tests. It helps catch import mistakes and matches modern packaging guidance.
Should I use requirements.txt or pyproject.toml?
Use requirements.txt for simple learning projects. Use pyproject.toml for modern projects, packages, tools, and uv-based workflows.
Should I commit .venv?
No. Commit dependency files, not the virtual environment folder.
What should every Python project have?
At minimum: README.md, .gitignore, dependency instructions, source code, and a clear run command. Add tests and pyproject.toml as the project grows.
27. Next Steps
Read these next:
python-project-folder-structure-guidepython-virtual-environments-venv-requirements-guideuv-python-package-manager-guidepython-testing-pytest-best-practices-guidepython-ruff-formatting-pre-commit-guidepython-configuration-logging-secrets-guide
Python project best practices are not about copying a perfect template. They are about making the project understandable, reproducible, testable, and safe to share.
