How the Hell Do You Structure Your Python Projects?

Kyan Machiels
4 min readJul 24, 2023

--

Introduction

As developers, we’ve all been there — starting a new project with enthusiasm, only to find ourselves tangled in a web of disorganized files and confusing dependencies. The lack of a clear project structure can quickly turn development into a nightmare. In this article, we’ll dive deep into the art of structuring projects in a way that promotes maintainability, scalability, and ease of collaboration. We’ll cover best practices, code samples, and step-by-step walkthroughs to help you navigate this common developer conundrum.

1. Why Project Structure Matters

A well-organized project structure is the foundation of a successful software development process. It impacts productivity, code maintainability, and the ability to scale the project in the future. A clear structure helps developers quickly locate relevant files, understand the project’s architecture, and collaborate effectively with team members.

2. Defining a Project Structure

The first step is to decide on a project structure that aligns with the specific needs of your project and the technology stack you’re using. Different programming languages and frameworks have their conventions, but you can always adapt and customize them according to your requirements.

3. Organizing Your Codebase

3.1 The “src” Directory

Most modern projects adopt a “src” (source) directory to hold the main source code of the project. This provides a clean separation from configuration files, documentation, and other non-code assets. Here’s a basic example of a project structure:

project_root/
├── src/
│ ├── main.py
│ ├── module_a.py
│ └── module_b.py
├── config/
├── tests/
└── README.md

3.2 Grouping by Features or Modules

Organizing code based on features or modules can enhance maintainability. Each feature or module is contained within its directory, encapsulating all related files. For instance:

project_root/
├── src/
│ ├── user_management/
│ │ ├── models.py
│ │ ├── views.py
│ │ └── utils.py
│ ├── data_processing/
│ └── ...

3.3 Shared Code and Utilities

Commonly used functions and utilities can be placed in a separate directory to promote code reusability. This directory can be named “utils,” “common,” or “shared.”

project_root/
├── src/
│ ├── user_management/
│ ├── data_processing/
│ └── utils/
│ ├── helper_functions.py
│ └── constants.py

3.4 Configurations and Environment Variables

Configuration files, along with environment variables, should be stored separately to maintain a clear distinction between code and settings. For instance:

project_root/
├── src/
├── config/
│ ├── settings.py
│ ├── database.yaml
│ └── ...
├── tests/
└── README.md

4. Managing Dependencies

Properly managing dependencies is critical to avoid version conflicts and ensure a smooth development experience.

4.1 Package Management

Use a package manager suitable for your programming language. For Python, you can utilize pip and a requirements.txt file to list dependencies.

project_root/
├── src/
├── requirements.txt
├── config/
├── tests/
└── README.md

4.2 Virtual Environments

Create a virtual environment for your project to isolate its dependencies from the system-wide packages. This helps to avoid conflicts between different projects

$ python -m venv my_project_venv
$ source my_project_venv/bin/activate # On Windows, use 'my_project_venv\Scripts\activate'

5. Testing and Quality Assurance

Testing and ensuring code quality are vital aspects of any project development. Let’s explore how to set up a solid testing environment and maintain code quality.

5.1 Unit Tests and Integration Tests

Unit tests and integration tests help validate the correctness of your code and its interactions with other components. For Python projects, you can use the built-in unittest library or popular testing frameworks like pytest.

Here’s an example of a basic unit test using unittest:

# project_root/tests/test_module_a.py

import unittest
from src.module_a import add
class TestModuleA(unittest.TestCase):
def test_add(self):
result = add(2, 3)
self.assertEqual(result, 5)

if __name__ == "__main__":
unittest.main()

For more complex projects, consider organizing tests in separate directories to reflect the project structure and naming test files with a prefix like test_ for easier discovery.

5.2 Linters and Code Formatters

Linters and code formatters play a significant role in maintaining a consistent code style and detecting potential issues. For Python, two popular tools are flake8 for linting and black for code formatting.

To install these tools, you can add them to your requirements.txt file:

# requirements.txt

flake8
black

After installing the dependencies, you can run the linter and formatter from the command line:

$ flake8 src  # Lint the 'src' directory
$ black src # Format the code in the 'src' directory

Integrating these tools into your development workflow, editor, or CI/CD pipeline ensures code quality remains consistent throughout the project.

5.3 Continuous Integration

Setting up Continuous Integration (CI) helps automate testing and code validation, enabling early detection of bugs and issues.

Popular CI services like GitHub Actions, CircleCI, or Jenkins allow you to define workflows that trigger tests and linters whenever changes are pushed to your version control system.

Here’s a simplified example of a GitHub Actions workflow for a Python project:

# .github/workflows/python.yml

name: Python CI

on:
push:
branches:
- main

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2

- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x' # Replace 'x' with your desired Python version

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Lint code
run: flake8 src

- name: Run tests
run: python -m unittest discover -s tests

This workflow will be triggered automatically whenever new commits are pushed to the main branch, helping you catch potential issues early and ensuring the project is always in a good state.

Conclusion

A well-structured project, backed by robust testing and quality assurance practices, is crucial for successful software development. By incorporating testing, linting, and continuous integration into your workflow, you can confidently maintain a high standard of code quality and deliver reliable software to your users. Happy coding!

--

--

Responses (1)