Python Project Structure: venv, pip, Requirements, and Testing
A single-file script is great for learning. But once your code has several functions, modules, data files, dependencies, and tests, you need a cleaner project structure.
Project structure is not only about looking professional. A clear structure makes a project easier to rerun, easier for others to read, and safer as dependencies grow.
Think of structure as a work map. Source code has its place, tests have their place, dependencies are recorded, and the README explains the commands to run. With these boundaries, another person does not have to guess which file to open first.
A simple folder structure
For a small project, use a structure like this:
expense-tracker/
src/
expense_tracker/
__init__.py
app.py
storage.py
tests/
test_app.py
requirements.txt
README.md
src/expense_tracker/ contains the main source code. tests/ contains tests. requirements.txt records dependencies. README.md explains how to run the project.
You do not need this structure for every tiny exercise, but it is a solid pattern once a project becomes more serious.
Creating a virtual environment
A virtual environment separates project dependencies from your global Python installation. This keeps one project from breaking another.
A virtual environment is not a new Python version. It is an isolated workspace with its own package location and executables, so pytest and other packages for this project do not mix with a different project.
python -m venv .venv
Activate it on macOS or Linux:
source .venv/bin/activate
Activate it in Windows PowerShell:
.\.venv\Scripts\Activate.ps1
After activation, the terminal usually shows (.venv) at the start of the prompt.
Installing packages with pip
Use pip to install packages.
pip install pytest
To see installed packages:
pip list
Avoid installing project dependencies into global Python. Activate .venv first.
requirements.txt
After installing dependencies, save the list:
pip freeze > requirements.txt
Someone else can install the same dependencies with:
pip install -r requirements.txt
For beginner projects, requirements.txt is enough. Later, you can learn modern packaging with pyproject.toml.
.env and configuration
A .env file is often used for configuration that changes between environments, such as API keys, database URLs, or app mode.
Example .env:
APP_ENV=development
DATA_FILE=expenses.json
Do not commit real secrets to a repository. For practice projects, create a .env.example file so others know which variables are needed.
Separating source code and tests
In src/expense_tracker/app.py:
# File: app-total.py
def calculate_total(expenses):
return sum(expense["amount"] for expense in expenses)
In tests/test_app.py:
# File: test_app.py
from expense_tracker.app import calculate_total
def test_calculate_total():
expenses = [
{"title": "Coffee", "amount": 25_000},
{"title": "Lunch", "amount": 50_000},
]
assert calculate_total(expenses) == 75_000
Tests help keep important logic correct as the code changes.
Start with tests for the most important behavior. A normal case protects the main flow, while empty or invalid cases clarify the program’s boundaries. Test names should describe behavior rather than implementation details, so they remain useful when the internals change.
Running pytest
Install pytest:
pip install pytest
Run tests:
pytest
When using a src/ structure, you may need to install the project in editable mode or configure PYTHONPATH. For a beginner project, the simplest path is to run commands from the project root and make sure the package can be imported.
A simple entry point
Use a main() function to keep the main file tidy.
# File: main.py
def main():
print("Expense tracker")
if __name__ == "__main__":
main()
This pattern lets the file run directly while still being safe to import from tests or other modules.
Mini structure example
src/expense_tracker/app.py:
# File: app-helpers.py
def calculate_total(expenses):
return sum(expense["amount"] for expense in expenses)
def format_currency(amount):
return f"IDR {amount:,}".replace(",", ".")
src/expense_tracker/storage.py:
# File: storage.py
import json
from pathlib import Path
def load_expenses(path):
file_path = Path(path)
if not file_path.exists():
return []
with file_path.open("r", encoding="utf-8") as file:
return json.load(file)
src/expense_tracker/main.py:
# File: main-report.py
from expense_tracker.app import calculate_total, format_currency
from expense_tracker.storage import load_expenses
def main():
expenses = load_expenses("expenses.json")
total = calculate_total(expenses)
print(format_currency(total))
if __name__ == "__main__":
main()
This structure is small, but the boundaries are clear: app.py for logic, storage.py for files, and main.py for program flow.
Common mistakes
The first mistake is committing the .venv folder. Add .venv/ to .gitignore.
The second mistake is forgetting to record dependencies. If a project needs pytest, requests, or another package, make sure it is listed in requirements.txt.
The third mistake is placing all logic in main.py. That is fine at the start, but split it once the file grows.
The fourth mistake is only writing tests after the project breaks. Start with small tests for the most important functions.
Small Python project checklist
Before sharing a project, check:
- The project has a
README.md. - Dependencies are recorded in
requirements.txt. .venv/is not committed.- There is a clear
src/folder or package. - There is a
tests/folder. - The command to run the project is documented in the README.
- Important logic has at least one test.
Mini exercise
Create an expense-tracker project with:
- A
.venvvirtual environment. - An
expense_trackerpackage. - A
calculate_total(expenses)function. - A
format_currency(amount)function. - Tests for both functions.
- A
README.mdwith install and test commands.
Checklist before moving on
Try copying the project into another folder and follow the README from the beginning. If it only works because of a personal terminal setting, the documentation is incomplete. The activation command, dependency installation, and test command should all be clear.
Before moving to the mini project, remove one unused dependency and run the tests again. This shows that requirements.txt describes project needs rather than every package that has ever been installed on your computer.
Next: CLI mini project
Now the pieces are ready: basic syntax, functions, files, error handling, modules, OOP, project structure, and tests. In the final article of this series, we will combine them into a CLI expense tracker. Continue with Python CLI Mini Project: Build an Expense Tracker from Scratch.
If you want to review OOP, open Object-Oriented Python for Beginners.
Related Articles
Keep reading within the same topic.
Python Fundamentals Learning Path: Series Recap and Next Steps
A practical recap of the Python Fundamentals series, with six lessons, key skills, checkpoints, and clear next steps after building the CLI project.
Python CLI Mini Project: Build an Expense Tracker from Scratch
Build a Python CLI expense tracker from scratch using input, JSON file storage, functions, dataclasses, error handling, and basic testing.
Object-Oriented Python for Beginners: Classes, Objects, and Dataclasses
Learn practical Python OOP: classes, objects, methods, __init__, dataclasses, composition, and when object-oriented code actually helps.
Python Files, Errors, and Modules: Writing Cleaner Scripts
Learn how to read files, write files, handle errors, create modules, import your own code, and organize cleaner Python scripts.