← Back to list

Modern Python Project Initialization

Welcome to 2026. The ecosystem has matured. Thanks to a new wave of tooling primarily from Astral we can now spin up a robust…

Hasith Vikasitha Dharmarathna · 2025-12-19 13:10 · 0 claps · 4.5 min read
#python #uv #pre-commit #ruff #best-practices
Open on Medium ↗

Modern Python Project Initialization

Welcome to 2026. The ecosystem has matured. Thanks to a new wave of tooling primarily from Astral we can now spin up a robust, standardized, and incredibly fast Python project in minutes.

This guide is opinionated. We are ditching the old ways for a stack that prioritizes speed, determinism, and developer experience.

The Modern Stack:

  1. **uv:** The unified Python package and project manager. It replaces pip, poetry, virtualenv, and pyenv entirely. It is shockingly fast.
  2. **ruff:** An extremely fast Python linter and formatter written in Rust.
  3. **mypy:** Static type checking to catch logic errors before runtime.
  4. **pre-commit:** The gatekeeper that ensures bad code never enters your repository.

Let’s build a professional-grade scaffold from scratch.

0. Prerequisites

You only need one tool installed globally on your machine: uv.

# On macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# On Windows
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"

Verify it worked: uv --version

Step 1: The Foundation (Git and Python Version)

We start empty. Create your directory and initialize git.

Bash

mkdir my-modern-app
cd my-modern-app
git init
# Good practice: name main branch 'main'
git branch -M main

The Critical .python-version File

Before we write code, we must agree on the Python version. If I use 3.12 and you use 3.10, things will break.

We solve this with a single file that uv respects automatically.

Bash

echo "3.12" > .python-version

Why this matters: When any developer (or your CI server) enters this directory and runs a uv command, uv will automatically fetch and use Python 3.12. No manual downloading required.

Step 2: Initialize the Project Structure

We will use uv to scaffold the project. We are aiming for the "src layout"—the industry standard for robust packaging, where project code lives in a distinct src/ subdirectory.

Bash

# Initialize a library structure named 'my_app'
uv init --lib --name my_app

Your folder now looks like this:

my-modern-app/
├── .git/
├── .python-version
├── README.md
├── pyproject.toml  <-- The heart of modern Python
└── src/
    └── my_app/
        ├── __init__.py
        └── py.typed <-- Marker for type checker support

uv has automatically created a virtual environment for you in a hidden .venv folder. You never need to manually create or activate environments again.

Step 3: The Toolchain (Dependencies)

Now we add our development tools. Notice the --dev flag. These tools are needed to build the project, but not to run the final application.

Bash

# The holy trinity of modern Python quality
uv add --dev ruff mypy pytest pre-commit

What just happened?

  • uv resolved compatible versions of these tools lightning-fast.
  • It installed them into the isolated project environment.
  • It added them to pyproject.toml under dependency groups.
  • Crucially: It created uv.lock. This file must be committed to git. It ensures every developer on your team is using the exact same version of every sub-dependency down to the hash.

Step 4: Configuration (The “Secret Sauce”)

Tools are useless without configuration. In modern Python, nearly everything goes into pyproject.toml.

Open pyproject.toml. It already has the basics. We need to add strict configurations for Ruff and Mypy to ensure high quality.

Append the following to your pyproject.toml:

Ini, TOML

# --- RUFF CONFIGURATION ---
[tool.ruff]
# Target Python 3.12 specifically
target-version = "py312"
# A reasonable line length for modern screens
line-length = 100
[tool.ruff.lint]
# Enable Pyflakes (`F`), pycodestyle (`E`, `W`), isort (`I`), 
# and Bugbear (`B`) for catching common pitfalls.
select = ["E", "F", "I", "B", "UP"]
ignore = []
# Allow fix for all enabled rules (when `--fix` is passed).
fixable = ["ALL"]
[tool.ruff.format]
# Use double quotes for strings.
quote-style = "double"
# Use spaces instead of tabs.
indent-style = "space"
# --- MYPY CONFIGURATION (TYPE CHECKING) ---
[tool.mypy]
# Be strict. This is the way.
strict = true
# Don't complain if a third-party library doesn't have type hints
ignore_missing_imports = true
# Don't check the tests directory for strict typing
exclude = ["tests"]

Step 5: The Gatekeeper (pre-commit)

We don’t want to rely on humans remembering to run linters. We use pre-commit to run checks automatically whenever someone types git commit. If the checks fail, the commit is blocked.

Create a new file named .pre-commit-config.yaml in the root directory:

YAML

repos:
  # Standard file fixers
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files
  # Ruff - Linting and Formatting in one go
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.4.4
    hooks:
      # Run the linter and automatically fix simple issues
      - id: ruff
        args: [ --fix ]
      # Run the formatter
      - id: ruff-format
  # Mypy - Static Type Checking
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.10.0
    hooks:
      - id: mypy
        additional_dependencies: [] # Add types-requests etc here if needed

Finally, install these hooks into your local .git directory:

Bash

uv run pre-commit install

Tip: Run uv run pre-commit run --all-files right now to see it in action. It might modify your yaml file slightly to fix formatting.

Step 6: Final Polish & First Commit

Let’s add a basic test file to ensure pytest is working.

Create tests/test_basic.py:

Python

def test_smoke():
    """A simple smoke test to verify the test harness works."""
    assert True

Let’s verify our entire harness works using uv run. This command executes tools within the project's isolated environment.

  • Lint: uv run ruff check .
  • Format: uv run ruff format .
  • Type Check: uv run mypy .
  • Test: uv run pytest

If those all pass, you are ready.

Bash

git add .
git commit -m "chore: initial project scaffold with uv, ruff, and mypy"
# git remote add origin <your-repo-url>
# git push -u origin main

The “Day 2” Workflow

Congratulations, you have a state-of-the-art Python environment. But how do you use it daily?

1. Adding a production library (e.g., FastAPI): Do not use pip. Use uv.

Bash

uv add fastapi
# This adds it to pyproject.toml AND updates uv.lock automatically

2. Running a script: You don’t need to activate the virtual environment.

Bash

uv run python src/my_app/main.py

3. The Daily Routine: Write code. Before you commit, you might run tests manually:

Bash

uv run pytest

When you commit, pre-commit will automatically run Ruff and Mypy to ensure you didn't break anything.

Summary

We have replaced a half-dozen brittle tools with a cohesive, lightning-fast workflow.

  • uv manages Python versions, environments, and dependencies deterministically
  • Ruff keeps code readable and standard.
  • Mypy catches bugs before they run.
  • pre-commit ensures no one bypasses the rules.

This setup takes 5 minutes to implement but saves hundreds of hours of debugging and configuration headaches down the road. Welcome to modern Python.


메타데이터
post_id
d8a31a6e9bf5
slug
modern-python-project-initialization-d8a31a6e9bf5
url
https://medium.com/@hasithvikasitha/modern-python-project-initialization-d8a31a6e9bf5
canonical_url
https://medium.com/@hasithvikasitha/modern-python-project-initialization-d8a31a6e9bf5
author_url
https://medium.com/@hasithvikasitha
status
ok
fetched_at
2026-06-16 19:09:56