Rain Lag

Leveling Up Your Side Project: Turning a Simple Script into a Maintainable Python Application

Learn how to transform a quick-and-dirty Python script into a clean, maintainable application using virtual environments, project structure, documentation, version control, testing, and packaging tools.

Introduction

Most Python projects start the same way: a single file, a clever idea, and the feeling of "I’ll just throw this together in an afternoon." A week later, that quick script has turned into a mission-critical tool for your team (or for you), and suddenly:

  • You’re afraid to change anything because it might break.
  • No one (including you) remembers how to install the dependencies.
  • Adding new features feels like walking through a minefield.

The difference between a fragile script and a maintainable application isn’t magic—it’s a set of habits and simple tools.

This post will walk you through how to level up your side project from a single Python file into a small but serious application. We’ll cover:

  • Virtual environments and reproducible installs
  • Clean project structure
  • Naming, docstrings, and self-documenting code
  • Tracking dependencies with requirements.txt
  • Version control with Git
  • Automated testing, linting, and formatting
  • Packaging and distribution options

You don’t need to adopt everything at once. Think of this as a roadmap: start with the basics and grow as your project grows.


1. Start with a Virtual Environment

A virtual environment isolates your project’s dependencies from your global Python installation. That means:

  • No more “this worked on my machine” issues.
  • You can use different versions of libraries per project.
  • You can recreate the environment later (or on another machine).

A common approach is to use venv (built into Python):

# Create a virtual environment python -m venv .venv # Activate it (Linux/macOS) source .venv/bin/activate # Or on Windows .venv\Scripts\activate # Install packages into this environment pip install requests rich

Once activated, python and pip will refer to this isolated environment. Make it a habit to:

  • Create a virtual environment as the first step in any new project.
  • Add .venv/ (or whatever you name it) to your .gitignore so it’s not committed.

This one step will save you countless headaches later.


2. Give Your Project a Clear Structure

Your script might start as script.py on your desktop, but as it grows, you’ll want a structure that supports:

  • Multiple modules (files)
  • Tests
  • Configuration
  • Packaging later on

A simple, flexible layout for a small application might look like:

my_app/ ├─ src/ │ └─ my_app/ │ ├─ __init__.py │ ├─ cli.py │ ├─ core.py │ └─ utils.py ├─ tests/ │ ├─ __init__.py │ └─ test_core.py ├─ requirements.txt ├─ pyproject.toml # or setup.cfg/setup.py later if you package it ├─ README.md └─ .gitignore

Key ideas:

  • src/my_app/ holds your application code, not the project root. This helps catch import issues and mirrors how your application will be installed.
  • tests/ is where you’ll add automated tests.
  • README.md explains what the project does and how to run it.
  • The __init__.py files make these directories into proper Python packages.

You don’t need all of this from day one, but moving from a single flat file to a structured layout is one of the biggest “level up” moments for any side project.


3. Name Things Well and Add Docstrings

As your codebase grows, good naming and documentation become your best tools against confusion.

Descriptive Names

Prefer names that describe what something does, not how short you can make it:

  • process_invoice() over proc_inv()
  • download_report() over dr()
  • user_repository.py over db.py

Organize by responsibility: for example, put command-line logic in cli.py, business logic in core.py, and helper functions in utils.py (or more specific names as the project evolves).

Docstrings

Docstrings sit at the top of modules, functions, classes, and methods. They make your code self-documenting and help tools like IDEs provide autocompletion docs.

# src/my_app/core.py def calculate_discount(price: float, percentage: float) -> float: """Return the price after applying a percentage discount. Args: price: Original price of the item. percentage: Discount as a value between 0 and 100. Returns: The discounted price. """ return price * (1 - percentage / 100)

When you (or someone else) revisit this in six months, the intent is obvious.


4. Track Dependencies in requirements.txt

Once you start installing packages (e.g., requests, pydantic, rich), you need a way to record them.

A requirements.txt file lets others (and future you) install exactly what your project needs:

pip install -r requirements.txt

You can generate it from your virtual environment:

pip freeze > requirements.txt

This records exact versions, like:

requests==2.32.3 rich==13.9.1

Guidelines:

  • Update requirements.txt whenever you add or remove dependencies.
  • Commit it to version control so your environment is reproducible.
  • Consider separating runtime and dev dependencies (e.g., requirements.txt and requirements-dev.txt), especially if you use test or lint tools only during development.

Later, you can move to a pyproject.toml-based workflow or tools like Poetry, but requirements.txt is a simple and effective starting point.


5. Use Version Control from Day One

Version control isn’t just for teams; it’s for past you vs. future you.

Using Git gives you:

  • A timeline of changes
  • The ability to try experiments on branches
  • A safe way to roll back when something breaks
  • Easy collaboration (and backups) via GitHub, GitLab, etc.

Basic setup:

git init echo "*.pyc\n__pycache__/\n.venv/" > .gitignore git add . git commit -m "Initial commit: basic project structure"

As you work:

  • Commit small, logical changes with descriptive messages.
  • Use branches for features (feature/add-report-export) and bug fixes (fix/discount-rounding).
  • Push to a remote for backup and collaboration.

Version control makes refactoring and experimentation vastly less risky—which is exactly what you need as your script evolves into an application.


6. Add Tests, a Linter, and a Formatter

Once your script becomes important, manual testing isn’t enough. You need automated checks that run every time you change the code.

Automated Testing

A simple starting point is pytest:

pip install pytest

Create a test file, e.g., tests/test_core.py:

# tests/test_core.py from my_app.core import calculate_discount def test_calculate_discount(): assert calculate_discount(100, 10) == 90

Run it with:

pytest

As you add features or fix bugs, add tests. Over time, this test suite becomes a safety net for refactoring.

Linting and Formatting

Tools like linters and formatters keep your code clean and consistent automatically.

Common choices:

  • Flake8 or Ruff (linter: finds style issues, potential bugs)
  • Black (formatter: enforces a consistent code style)
  • isort (sorts imports)

Example setup:

pip install black ruff isort

Run them manually:

black src tests ruff src tests isort src tests

Or configure them in pyproject.toml so they remember your preferences.

You can also:

  • Add a pre-commit hook (using the pre-commit tool) so code is auto-formatted and linted before every commit.
  • Configure your editor or IDE to run formatters on save.

This reduces style debates and frees you to think about logic rather than spacing.


7. Package and Distribute Your Application

Once your side project becomes genuinely useful, you might want to:

  • Install it with pip install my-app on other machines.
  • Share it with colleagues or the community.
  • Give non-technical users a single executable file.

Python Packaging Basics

Modern Python packaging often centers around pyproject.toml. A minimal example for an installable CLI app:

[project] name = "my-app" version = "0.1.0" description = "A handy CLI tool that does X" readme = "README.md" requires-python = ">=3.10" [project.scripts] my-app = "my_app.cli:main"

With this in place and using a build backend like setuptools or hatchling, you can build and install your project locally:

pip install build python -m build pip install dist/my_app-0.1.0-py3-none-any.whl

Now you can run my-app from the command line as an installed command.

Templates and Tools

Packaging can be fiddly, so scaffolding tools and templates help you start with best practices:

  • pyOpenSci copier templates: provide opinionated, research-friendly project templates with tests, docs, and CI pre-wired.
  • Project templates from frameworks (e.g., fastapi or django-admin startproject) if your app fits those ecosystems.

These templates often include:

  • Standardized structure
  • Configuration for linters and formatters
  • Docs and CI examples

Single-File Executables

If you want to give non-Python users a single file to run:

  • Tools like PyInstaller, cx_Freeze, or py2exe-like bundlers can package your Python code and its dependencies into a standalone executable.
  • This is ideal for internal tools or desktop utilities where installing Python isn’t desirable.

Be aware these bundles can be large and platform-specific (one build for Windows, another for macOS, etc.), but they’re extremely convenient for end users.


Conclusion

Transforming a quick script into a maintainable Python application is less about rewriting everything and more about adding structure and safeguards:

  • Use a virtual environment to isolate dependencies.
  • Adopt a clear project structure so your code can grow without chaos.
  • Choose descriptive names and add docstrings so the code explains itself.
  • Track dependencies in requirements.txt for easy, reproducible installs.
  • Put everything under Git so you can experiment, roll back, and collaborate safely.
  • Add automated tests, a linter, and a formatter to keep quality high as the project evolves.
  • Explore packaging and bundling tools when you’re ready to share your app more broadly.

You don’t need to be working on a huge or “official” product to justify these practices. Applying them to your side projects makes them more enjoyable to work on, easier to share, and far less fragile.

Start with one improvement—maybe a virtual environment and Git today, tests and formatting next week—and keep leveling up. Your future self (and anyone who uses your project) will be grateful.

Leveling Up Your Side Project: Turning a Simple Script into a Maintainable Python Application | Rain Lag