Python Project Best Practices: Structure & Testing Guide

Jun 26, 2026
50 min read

AI Insights

Powered by GPT-4o-mini

Verified Context: python-project-best-practices-structure-testing-guide
Quick Answer

A complete Python project best practices guide covering folder structure, virtual environments, pyproject.toml, dependency management, pytest, Ruff, pre-commit, logging, configuration, secrets, Git, CI, and release checklists.

Quick Summary

Discover essential Python project best practices including structure, virtual environments, testing, and logging for effective development.

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:

text
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.

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, or uv
  • 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 TypeBest Starting Point
one-file practice scriptone .py file is fine
learning notebooknotebooks/, data/, README.md
data analysis projectnotebooks/, src/, data/raw, data/processed
CLI toolsrc/package_name/, pyproject.toml, tests/
web appsrc/ or app package, config module, tests, .env.example
package for reusesrc/ 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.

Use this for learning projects:

text
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.

Use this for apps, CLIs, and reusable code:

text
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.md

The 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:

bash
python -m venv .venv

Activate it:

bash
source .venv/bin/activate

Windows PowerShell:

powershell
py -m venv .venv
.\.venv\Scripts\Activate.ps1

Install 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:

text
pandas==2.3.0
requests==2.32.0
pytest==8.4.0

Install:

bash
python -m pip install -r requirements.txt

For modern app or package projects, prefer pyproject.toml.

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:

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:

text
DATABASE_URL=sqlite:///local.db
API_KEY=replace-with-local-secret

Do not commit it.

Commit .env.example:

text
DATABASE_URL=sqlite:///example.db
API_KEY=
LOG_LEVEL=INFO

This tells other developers which variables are required without leaking secrets.

9. Write A Useful README

Minimum useful README:

markdown

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

text

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.py

Better structure:

text
src/
  task_tracker/
    __init__.py
    main.py
    services.py
    storage.py

Run modules with:

bash
python -m task_tracker.main

Importable code is easier to test.

11. Separate Pure Logic From I/O

Pure logic is easier to test.

Weak version:

python
def calculate_total():
    price = float(input("Price: "))
    tax = price * 0.18
    print(price + tax)

Better:

python
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:

python
def test_total_with_tax():
    assert total_with_tax(100, 0.18) == 118

12. Add Tests Early

Use pytest.

bash
python -m pip install pytest

Recommended layout:

text
tests/
  test_services.py
  test_config.py

Test files should start with test_.

Test functions should start with test_.

Run:

bash
pytest

At 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:

bash
python -m pip install ruff

Run checks:

bash
ruff check .

Format:

bash
ruff format .

Example pyproject.toml config:

toml
[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.

python
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:

python
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:

python
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)

Use:

  • debug for developer details
  • info for normal lifecycle events
  • warning for recoverable problems
  • error for failed operations
  • exception inside exception handlers

16. Centralize Configuration

Avoid scattering environment lookups everywhere.

Weak:

python
import os

db_url = os.environ["DATABASE_URL"]

Better:

python
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:

python
API_KEY = "hardcoded-placeholder"

Better:

python
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/.

text
scripts/
  seed_data.py
  export_report.py
  migrate_local.py

Scripts should be safe, documented, and idempotent when possible.

Avoid keeping one-off scratch files forever.

19. Write Small Commits

Good commit examples:

text
Add project config loader
Add pytest coverage for CSV parser
Fix missing value handling in importer

Avoid commits like:

text
changes
final
misc fixes

Small commits make rollback and review easier.

20. Add Pre-Commit Hooks When The Project Grows

Pre-commit runs checks before commits.

Install:

bash
python -m pip install pre-commit

Example .pre-commit-config.yaml:

yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.5.0
    hooks:
      - id: ruff
      - id: ruff-format

Install hooks:

bash
pre-commit install

Use 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:

yaml
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: pytest

CI is especially useful when other people depend on the project.

22. Keep Data And Outputs Under Control

For data projects:

text
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.

text
docs/
  decisions.md

Example:

markdown

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.

text

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-guide
  • python-virtual-environments-venv-requirements-guide
  • uv-python-package-manager-guide
  • python-testing-pytest-best-practices-guide
  • python-ruff-formatting-pre-commit-guide
  • python-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.

Frequently Asked Questions

What is a good Python project structure for serious projects?
A good Python project structure includes a README.md, pyproject.toml, .gitignore, .env.example, src/ directory with main modules, tests/, scripts/, docs/, and a .venv/ directory.
What should not be committed to a Python project's repository?
Do not commit .venv, .env, caches, logs, secrets, or large generated files to a Python project's repository.
What are the properties of a good Python project?
A good Python project has a clear entry point, predictable folders, isolated dependencies, documented setup steps, repeatable test commands, no committed secrets, readable configuration, useful logs, small commits, and easy onboarding for another developer.
How should a beginner structure a learning Python project?
A beginner should structure a learning Python project with a README.md, requirements.txt, .gitignore, src/ directory for code, data/ for data files, tests/ for test files, and a .venv/ for the virtual environment.
What is the purpose of using a pyproject.toml file in a Python project?
The pyproject.toml file is used for packaging and managing project dependencies and configurations in a Python project.

Related Work

See how this thinking shows up in shipped systems.