Project Organization and Distribution
By completing this module you will be able to:
- Structure Python projects professionally
- Create reusable packages with organized modules
- Write effective documentation (README, docstrings)
- Apply style standards (PEP8) and use quality tools
- Prepare projects for sharing or publishing
From script to project: the professional leap
When we start programming, everything fits in a single file. A calculate.py script with 50 lines solves the problem and that’s it. But as the project grows, that file becomes a 2000-line monster where nobody can find anything.
It’s like moving into your first apartment. At first, everything fits in one suitcase: clothes, books, some stuff. But when your life grows, you need to organize: closets for clothes, shelves for books, labeled drawers for documents. A Python project is the same: at first a script is enough, but when you grow you need organized folders, separate modules and clear documentation.
Why does organization matter?
- For future you: In 6 months you won’t remember what each function does
- For others: If someone wants to use or contribute to your code, they need to understand it
- For tools: Tests, linters and automatic documentation expect a certain structure
- For installation: A well-organized project installs with
pip install
Structure of a Python project
Think of a well-designed building: each floor has a purpose. Offices on one, apartments on another, parking in the basement. You don’t mix the laundry room with the boardroom. Your Python project should be the same: each folder has a clear purpose.
Recommended structure (modern)
my_project/
├── src/
│ └── my_package/
│ ├── __init__.py
│ ├── core.py
│ ├── utils.py
│ └── cli.py
├── tests/
│ ├── __init__.py
│ ├── test_core.py
│ └── test_utils.py
├── docs/
│ └── index.md
├── pyproject.toml
├── README.md
├── LICENSE
└── .gitignoreWhat is each thing?
| Folder/File | Purpose |
|---|---|
| src/ | Project source code (optional but recommended) |
| my_package/ | Your Python package with modules |
| init.py | Converts the folder into an importable package |
| tests/ | Automated tests (pytest) |
| docs/ | Project documentation |
| pyproject.toml | Project configuration (dependencies, metadata) |
| README.md | Project presentation |
| LICENSE | Usage license |
| .gitignore | Files that Git should ignore |
Simplified structure (for small projects)
If your project is small, you can omit src/:
my_project/
├── my_package/
│ ├── __init__.py
│ └── core.py
├── tests/
│ └── test_core.py
├── pyproject.toml
├── README.md
└── .gitignoreModules and packages: organizing code
Think of a library: it organizes its books on shelves (packages), which are in sections (folders), and each book is a module (.py file). The librarian (Python) knows how to find any book if you tell them the section and shelf.
What is a module?
A module is simply a .py file. When you write import math, you’re importing the math.py module.
1# file: utils.py (this is a module)
2def greet(name):
3 return f"Hello, {name}"
4
5PI = 3.14159What is a package?
A package is a folder with an __init__.py file. It groups related modules.
calculator/
├── __init__.py
├── basic.py # add, subtract
├── scientific.py # sqrt, power
└── financial.py # interest, amortizationThe __init__.py file
This special file does two things:
- Converts the folder into an importable package
- Defines what is exported when someone imports the package
1# calculator/__init__.py
2
3# Import specific functions for direct access
4from .basic import add, subtract
5from .scientific import square_root
6
7# Define what's exported with "from calculator import *"
8__all__ = ["add", "subtract", "square_root"]
9
10# Package version
11__version__ = "1.0.0"Now you can do:
1from calculator import add # Direct, thanks to __init__.py
2from calculator.financial import compound_interest # Access to moduleAbsolute vs relative imports
Specify the full path from the package root. Recommended for clarity.
1# From anywhere in the project
2from my_package.utils import format_data
3from my_package.core import processUse dots to indicate relative position. Useful within the same package.
1# From my_package/core.py
2from .utils import format_data # same level (.)
3from ..other_package import something # parent level (..)pyproject.toml: the project’s passport
Your passport contains all your official information: name, nationality, date of birth. pyproject.toml is your project’s passport: name, version, author, dependencies… Everything anyone who finds it needs to know.
Basic structure
1[project]
2name = "my-package"
3version = "1.0.0"
4description = "A short project description"
5readme = "README.md"
6license = {text = "MIT"}
7authors = [
8 {name = "Your Name", email = "[email protected]"}
9]
10requires-python = ">=3.9"
11dependencies = [
12 "requests>=2.28.0",
13 "click>=8.0.0",
14]
15
16[project.optional-dependencies]
17dev = [
18 "pytest>=7.0.0",
19 "black>=23.0.0",
20 "flake8>=6.0.0",
21]
22
23[project.scripts]
24my-command = "my_package.cli:main"
25
26[build-system]
27requires = ["setuptools>=61.0"]
28build-backend = "setuptools.build_meta"
29
30[tool.pytest.ini_options]
31testpaths = ["tests"]
32
33[tool.black]
34line-length = 88
35
36[tool.isort]
37profile = "black"Important sections
| Section | Purpose |
|---|---|
| [project] | Project metadata (name, version, author) |
| dependencies | Packages your project needs to work |
| [project.optional-dependencies] | Optional dependencies (e.g., for development) |
| [project.scripts] | Terminal commands your package installs |
| [build-system] | How to build the package for distribution |
| [tool.*] | Tool configuration (pytest, black, etc.) |
Before, setup.py was used to configure projects. Although it still works, pyproject.toml is the modern standard (PEP 517/518). If you see tutorials with setup.py, the concept is the same but the syntax differs.
README: the first impression
When you arrive at a store, the entrance sign tells you what they sell and why you should come in. The README is that sign: the first thing visitors to your project see.
Recommended structure
1# Project Name
2
3Brief description (1-2 sentences) of what it does and for whom.
4
5## Installation
6
7```bash
8pip install my-packageQuick Start
1from my_package import main_function
2
3result = main_function("data")
4print(result)Features
- Feature 1
- Feature 2
- Feature 3
Documentation
For complete documentation, visit [link to docs].
Contributing
Contributions are welcome. Please read CONTRIBUTING.md.
License
MIT License - see LICENSE for details.
Tips for a good README
- Start with the “what”: Clear description in the first lines
- Show, don’t tell: A code example is worth more than paragraphs of explanation
- Simple installation: The reader should be able to install in 30 seconds
- Keep it updated: An outdated README is worse than none
Code style and quality
In a professional job there are dress code rules: not because clothes affect your productivity, but because it facilitates collaboration and looks good. PEP8 is Python’s “dress code”: a set of conventions we all follow so code is readable and consistent.
PEP8: the official style guide
PEP8 defines how Python code should look. Some rules:
- Indentation with 4 spaces (not tabs)
- Maximum 79-88 characters per line
- Spaces around operators:
x = 1 + 2, notx=1+2 - Names:
snake_casefor functions and variables,PascalCasefor classes - Two blank lines between top-level functions
Automatic tools
Why memorize rules when tools can do it for you?
Automatic formatter - Reformats your code automatically. “Opinionated”: makes decisions for you.
1pip install black
2black my_package/ # Formats all filesBefore:
1x=1+2
2lista=[1,2,3,4,5,6,7,8,9,10,11,12]After:
1x = 1 + 2
2lista = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]Sorts imports - Groups and sorts your imports automatically.
1pip install isort
2isort my_package/Before:
1import os
2from my_package import something
3import sys
4from collections import defaultdictAfter:
1import os
2import sys
3from collections import defaultdict
4
5from my_package import somethingLinter - Detects style errors and potential problems.
1pip install flake8
2flake8 my_package/Output:
my_package/core.py:15:1: E302 expected 2 blank lines, found 1
my_package/utils.py:23:80: E501 line too long (95 > 79 characters)ruff is a modern and faster alternative:
1pip install ruff
2ruff check my_package/Type checker - Verifies that type hints are correct.
1pip install mypy
2mypy my_package/1def add(a: int, b: int) -> int:
2 return a + b
3
4add("hello", "world") # mypy: error!Configuration in pyproject.toml
1[tool.black]
2line-length = 88
3target-version = ['py39']
4
5[tool.isort]
6profile = "black"
7line_length = 88
8
9[tool.ruff]
10line-length = 88
11select = ["E", "F", "W"]
12
13[tool.mypy]
14python_version = "3.9"
15warn_return_any = trueDocstrings: documenting code
A good chef doesn’t just cook: they leave notes explaining their recipes so others can reproduce them. Docstrings are those notes in your code: they explain what each function does, what parameters it receives, and what it returns.
Docstring styles
1def calculate_discount(price: float, percentage: float) -> float:
2 """Calculates the price with discount applied.
3
4 Args:
5 price: Original product price.
6 percentage: Discount percentage (0-100).
7
8 Returns:
9 Final price after applying the discount.
10
11 Raises:
12 ValueError: If percentage is not between 0 and 100.
13
14 Example:
15 >>> calculate_discount(100, 20)
16 80.0
17 """
18 if not 0 <= percentage <= 100:
19 raise ValueError("Percentage must be between 0 and 100")
20 return price * (1 - percentage / 100) 1def calculate_discount(price, percentage):
2 """
3 Calculates the price with discount applied.
4
5 Parameters
6 ----------
7 price : float
8 Original product price.
9 percentage : float
10 Discount percentage (0-100).
11
12 Returns
13 -------
14 float
15 Final price after applying the discount.
16
17 Raises
18 ------
19 ValueError
20 If percentage is not between 0 and 100.
21 """
22 if not 0 <= percentage <= 100:
23 raise ValueError("Percentage must be between 0 and 100")
24 return price * (1 - percentage / 100)Type hints: living documentation
Type hints document expected types and enable automatic verification:
1from typing import Optional, List
2
3def find_user(
4 name: str,
5 min_age: Optional[int] = None
6) -> List[dict]:
7 """Finds users by name and optionally by age."""
8 ....gitignore: what NOT to upload
Some files should never be uploaded to Git:
# Python bytecode
__pycache__/
*.py[cod]
*.pyo
# Virtual environments
venv/
.venv/
env/
# IDE configuration
.vscode/
.idea/
*.swp
# Distribution files
dist/
build/
*.egg-info/
# Tests and coverage
.pytest_cache/
.coverage
htmlcov/
# Environment variables and secrets
.env
.env.local
*.pem
secrets.json
# System files
.DS_Store
Thumbs.dbPasswords, API keys, tokens… If you ever upload a secret to Git, consider it compromised even if you delete it later. Git keeps history.
Preparing to share
Install in editable mode
During development, install your package in “editable” mode:
1pip install -e .This creates a link to the source code. Changes you make are reflected immediately without reinstalling.
Create distributions
To share your package:
1pip install build
2python -m buildThis creates in dist/:
my_package-1.0.0.tar.gz- Compressed source codemy_package-1.0.0-py3-none-any.whl- Wheel (fast installation)
Publish to PyPI
So anyone can do pip install your-package:
1pip install twine
2twine upload dist/*Before publishing to real PyPI, practice with TestPyPI:
1twine upload --repository testpypi dist/*Common licenses
| License | Allows | Requires |
|---|---|---|
| MIT | Almost everything | Keep copyright notice |
| Apache 2.0 | Almost everything + patents | Document changes |
| GPL | Almost everything | Derived code also GPL |
Professional project checklist
Before sharing your project, verify:
| Element | Got it? | Priority |
|---|---|---|
| README.md with installation and example | ☐ | High |
| pyproject.toml with dependencies | ☐ | High |
| tests/ folder with tests | ☐ | High |
| .gitignore configured | ☐ | High |
| Docstrings in public functions | ☐ | Medium |
| Type hints | ☐ | Medium |
| Linter configured (black/ruff) | ☐ | Medium |
| LICENSE | ☐ | Medium |
| CI/CD configured | ☐ | Low |
Practical Exercises
You have this monolithic script. Reorganize it into an appropriate project structure:
1# all_in_one.py
2import json
3
4def load_data(file):
5 with open(file) as f:
6 return json.load(f)
7
8def process(data):
9 return [d for d in data if d['active']]
10
11def save(data, file):
12 with open(file, 'w') as f:
13 json.dump(data, f)
14
15def main():
16 data = load_data('input.json')
17 processed = process(data)
18 save(processed, 'output.json')
19 print(f"Processed {len(processed)} records")
20
21if __name__ == '__main__':
22 main()Given this code with style problems, configure black, isort and ruff in pyproject.toml, and fix it:
1import sys,os
2from collections import defaultdict
3import json
4x=1+2
5def myFunction( a,b,c ):
6 return a+b+c
7class myClass:
8 def __init__(self,value):self.value=valueQuiz
🎮 Quiz: Project Organization
Before the practical ending
You now know the professional side of the workflow: folder structure, pyproject.toml, documentation, quality tools, and distribution. The next step is not more theory, but putting everything together in a guided project.
In the Final Project you will build a reusable mini library called contact_book, applying data structures, functions, file handling, light OOP, and tests.