Structuring a Real Project

Advanced · 18 min read · ▶ live playground · ✦ checkpoint

A script can survive as one file. A project cannot. Once your code has tests, settings, data files, and teammates, the folder layout becomes part of the program.

Good Python project structure makes three things boring: imports find the code you meant, settings live in one place, and secrets never sit in source code.

Start with a real project root

The project root is the top folder of one project, where a person should be able to read the files and understand how to run, test, and package the work. Think of it as the bench top in your workshop: tools may live in drawers, but the bench tells you what project you are working on.

A beginner project often grows flat:

snack-tracker/
|-- app.py
|-- reports.py
|-- settings.py
|-- test_reports.py
`-- .venv/

This works for a while. Then imports get muddy. A file named email.py can shadow Python's standard-library email module. Tests may pass only because the terminal is sitting in the project root, not because the project would work after installation.

The common professional shape is the src/ layout, a project layout where importable application code lives under a src folder instead of directly beside the project files:

snack-tracker/
|-- README.md
|-- pyproject.toml
|-- .gitignore
|-- .env.example
|-- src/
|   `-- snack_tracker/
|       |-- __init__.py
|       |-- __main__.py
|       |-- reports.py
|       `-- settings.py
`-- tests/
    `-- test_reports.py

The src/ folder is the code drawer. The package folder, snack_tracker/, holds modules the program imports. The tests/ folder is the checklist board you learned in Section 16. Project-level files stay at the root where tools can find them.

pyproject.toml is the standard Python project settings file; lesson 18.3 gives it the full treatment. For now, treat it as the place project tools read from. Your dependency lock from lesson 18.1 also belongs at the root.

Make imports honest

A package is a folder of related modules Python can import. You built packages in lesson 8.3; the src/ layout just gives that package a safer home.

Inside the package, keep code split by job:

def daily_message(player: str, apples: int) -> str:
    return f"{player} packed {apples} apples today."

The command entry point can stay tiny:

from snack_tracker.reports import daily_message
 
print(daily_message("Mina", 4))

__main__.py is the module Python runs when you run a package with python -m package_name. After the project tool has made your src/ package visible in the environment, the daily command shape becomes:

$ uv run python -m snack_tracker
$ uv run python -m pytest

Use your project's tool instead of uv if the project chose Poetry or pip-tools. The deeper habit is the same: run commands through the project environment, and import from the package that lives under src/.

Put settings in one module

Configuration is the set of values that change how a program runs without changing its code, such as a data-file path, log level, or app mode. Settings are the named configuration values your program reads.

Do not scatter those values across random modules. Give them one home:

import os
 
APP_MODE = os.environ.get("SNACK_TRACKER_MODE", "dev")
DATA_FILE = os.environ.get("SNACK_TRACKER_DATA_FILE", "data/snacks.json")
LOG_LEVEL = os.environ.get("SNACK_TRACKER_LOG_LEVEL", "INFO")

os.environ is a dictionary-like view of environment variables. An environment variable is an operating-system name/value setting handed to a program when it starts.

Other modules import names from settings.py instead of reading raw environment variables everywhere:

from snack_tracker.settings import APP_MODE
 
 
def report_title(player: str) -> str:
    return f"{player}'s snack report ({APP_MODE})"

This gives you one settings desk. When the app needs a new value, you add it there, name it clearly, and keep the rest of the code focused on behavior.

Keep local files out of history

Git is a version-control tool, a tool that records project history, and .gitignore is a file that lists paths Git should not save. Git gets its full lesson in Section 19, but the security habit starts now.

A Python starter .gitignore usually includes local environments, local secrets, caches, and build output:

.venv/
.env
__pycache__/
.pytest_cache/
.coverage
dist/

dist/ is build output you will meet in lesson 18.3. __pycache__/ is Python's bytecode cache. .pytest_cache/ and .coverage are tool data from testing. These files belong on your machine, not in the shared project record.

.env is a local text file many tools use to load environment variables for one machine. The safe pair is:

SNACK_TRACKER_MODE=dev
SNACK_TRACKER_DATA_FILE=data/snacks.json
SNACK_TRACKER_API_TOKEN=replace-me
SNACK_TRACKER_MODE=dev
SNACK_TRACKER_DATA_FILE=data/snacks.json
SNACK_TRACKER_API_TOKEN=real value from your account

The example file teaches humans which names exist. The real .env file supplies private values and stays out of history.

A starter structure checklist

For a small application project, this is enough structure:

  • Root files explain and configure the project: README.md, pyproject.toml, lockfile, .gitignore, .env.example.
  • Importable code lives under src/project_name/.
  • Tests live under tests/.
  • Settings are read in one module, usually settings.py.
  • Real secrets come from environment variables and never from committed files.

That may feel like ceremony for a tiny script. It pays off when the project leaves your laptop. Another person can create the environment, inspect the root, run the tests, and find the actual app code without guessing.

Checkpoint

Answer all three to mark this lesson complete

Now the project has a clean shape: environment first, files second, private values outside the code. Next, you turn that shaped project into a distributable Python package.

+50 XP on completion