Project Organization and Distribution

Module Objectives

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.

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
└── .gitignore

What 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
└── .gitignore

Modules 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.14159

What 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, amortization

The __init__.py file

This special file does two things:

  1. Converts the folder into an importable package
  2. 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 module

Absolute 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 process

Use 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.)
setup.py is legacy

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.

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-package

Quick 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, not x=1+2
  • Names: snake_case for functions and variables, PascalCase for 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 files

Before:

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 defaultdict

After:

1import os
2import sys
3from collections import defaultdict
4
5from my_package import something

Linter - 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 = true

Docstrings: 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.db
Never upload secrets

Passwords, 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 build

This creates in dist/:

  • my_package-1.0.0.tar.gz - Compressed source code
  • my_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/*
TestPyPI for practice

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

Exercise 1: Reorganize a script

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()
data_processor/
├── src/
│   └── processor/
│       ├── __init__.py
│       ├── io.py
│       ├── core.py
│       └── cli.py
├── tests/
│   ├── test_io.py
│   └── test_core.py
├── pyproject.toml
└── README.md
1# src/processor/__init__.py
2from .core import process
3from .io import load_data, save
4
5__all__ = ["process", "load_data", "save"]
 1# src/processor/io.py
 2import json
 3from pathlib import Path
 4
 5def load_data(file: str | Path) -> list[dict]:
 6    """Loads data from a JSON file."""
 7    with open(file) as f:
 8        return json.load(f)
 9
10def save(data: list[dict], file: str | Path) -> None:
11    """Saves data to a JSON file."""
12    with open(file, 'w') as f:
13        json.dump(data, f, indent=2)
1# src/processor/core.py
2def process(data: list[dict]) -> list[dict]:
3    """Filters active records."""
4    return [d for d in data if d.get('active', False)]
 1# src/processor/cli.py
 2from .io import load_data, save
 3from .core import process
 4
 5def main():
 6    data = load_data('input.json')
 7    processed = process(data)
 8    save(processed, 'output.json')
 9    print(f"Processed {len(processed)} records")
10
11if __name__ == '__main__':
12    main()
 1# pyproject.toml
 2[project]
 3name = "data-processor"
 4version = "1.0.0"
 5description = "JSON data processor"
 6requires-python = ">=3.10"
 7
 8[project.scripts]
 9process = "processor.cli:main"
10
11[build-system]
12requires = ["setuptools>=61.0"]
13build-backend = "setuptools.build_meta"
Exercise 2: Configure quality tools

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=value
 1# pyproject.toml
 2[tool.black]
 3line-length = 88
 4
 5[tool.isort]
 6profile = "black"
 7
 8[tool.ruff]
 9line-length = 88
10select = ["E", "F", "W", "I"]

After running black and isort:

 1import json
 2import os
 3import sys
 4from collections import defaultdict
 5
 6x = 1 + 2
 7
 8
 9def my_function(a, b, c):
10    return a + b + c
11
12
13class MyClass:
14    def __init__(self, value):
15        self.value = value

Quiz

🎮 Quiz: Project Organization

0 / 0
Loading questions...

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.

Next stop

In the Final Project you will build a reusable mini library called contact_book, applying data structures, functions, file handling, light OOP, and tests.


Previous: Testing Next: Final Project