# Essential Python Project Best Practices for Developers
URL: https://madhudadi.in/blog/posts/python-project-best-practices-structure-testing-guide
Published: 2026-06-26
Tags: python, Project Structure, Best Practices
Read time: 50 min
Difficulty: intermediate
> 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.# 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.

## Official Links

- Python virtual environments: https://docs.python.org/3/tutorial/venv.html
- Python packaging guide: https://packaging.python.org/
- PyPA `pyproject.toml` guide: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/
- PyPA `src` layout 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`, 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 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:

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

## 4. Recommended Professional Structure

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
# 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.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.
```

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.
