Structuring Your Python Project in 2025: A Modern Approach
If you've ever felt overwhelmed by the myriad ways to set up a Python project, you're in good company. Over the years, Python development has steadily evolved, and today (in 2025) it's all about modernizing your workflow. You might remember the days of juggling requirements.txt
files—while that method served its purpose back then, it's now time to embrace something more refined: pyproject.toml
.
You're not expected to grasp every nuance right away; think of this as a guided tour through the evolution of Python project structure, peppered with insights from both history and modern industry practices.
Moving Beyond requirements.txt
Back in the day, managing your project's dependencies often meant running pip install -r requirements.txt
. This approach, although familiar, is like using a paper map in the age of GPS. Today, pyproject.toml
stands as a centralized hub, compliant with standards like PEP 518 for build system requirements and PEP 621 for project metadata.
By consolidating configuration details in one file, you can keep your project neat and maintainable—just as you might organize your workspace to minimize distractions.
Here's a typical example of a pyproject.toml
:
[project]
name = "yet-another-fastapi-template"
version = "0.1.0"
description = """
A modern, opinionated FastAPI template for building Python web applications with best practices
and a well-structured codebase. It leverages a cutting-edge stack for optimal performance,
maintainability, and developer productivity, making it an ideal starting point for your next project.
"""
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"fastapi[standard]>=0.115.8",
"sqlalchemy>=2.0.37",
"asyncpg>=0.30.0",
"alembic>=1.14.1",
"python-dotenv>=1.0.1",
"pydantic-settings>=2.7.1",
"greenlet>=3.1.1",
"redis>=5.2.1",
"cryptography>=44.0.0",
"python-jose>=3.3.0",
]
[dependency-groups]
dev = [
"ruff>=0.9.4",
"pytest>=8.3.4",
"pytest-asyncio>=0.25.3",
"pytest-cov>=6.0.0",
"aiosqlite>=0.20.0",
"bandit[toml]>=1.8.2",
"pre-commit>=4.1.0",
]
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
asyncio_default_fixture_loop_scope = "function"
This setup puts everything in its place so you can focus on building great software without getting bogged down in configuration details.
This leads us to an important question. How can you efficiently manage your pyproject.toml
?
The answer is simple, modern Python package manager is all you need. Rather than managing manually, you can let the package manager do the work for you.
Top Candidate for Python Package Managers
Package Manager | Details |
---|---|
Poetry |
|
PDM |
|
UV |
|
Each of these tools automatically enforces the correct Python version by reading your pyproject.toml
, freeing you to focus on building great software without getting bogged down by manual configuration.
My personal favorite is UV.
Emphasizing Type Safety
Python's dynamic nature is both a blessing and a curse. While it gives you flexibility, it also leaves room for subtle bugs that only appear at runtime. If you're thinking, "Type safety sounds tedious," consider it the code equivalent of a spell-checker. It might flag a few red marks now and then, but these hints help prevent costly mistakes down the road.
To bolster your project's reliability, you can rely on a couple of key strategies:
-
LSP Integrations: Tools like Pylance, Pyright, and MyPy provide real-time type checking within your IDE, catching potential errors as you write.
-
Formatters and Linters: Several excellent tools are available:
- Ruff: A fast and versatile tool that performs both linting and formatting. A great all-in-one option.
- Black: A popular, uncompromising code formatter that enforces a consistent style.
- autopep8: Automatically formats Python code to conform to PEP 8 style guidelines.
- Flake8: A widely used linter that checks for style and programming errors.
- Pylint: A powerful linter that performs deep code analysis, including type checking and style violations. Offers more in-depth analysis than Flake8 but can be more verbose.
VSCodewith Python LSP (Language Server Protocol)
For example, a simple Pyright configuration for strict type checking might look like this:
{
"typeCheckingMode": "strict",
"reportMissingImports": true,
"exclude": ["venv", "build"]
}
Additionally, you also have the option to configure Ruff directly in your pyproject.toml
file—or in a separate Ruff configuration file if you prefer keeping things modular. If you decide to include Ruff's configuration in your pyproject.toml
, here is an example:
[tool.ruff]
line-length = 155
ignore = [
# (missing public docstrings) These work off of the Python sense of "public", rather than our
# bespoke definition based off of `@public`. When ruff supports custom plugins then we can write
# appropriate rules to require docstrings for `@public`.
"D100",
"D101",
"D102",
"D103",
"D104",
"D105",
"D106",
"D107",
]
select = [
# (flake8-builtins) detect shadowing of python builtin symbols by variables and arguments.
# Attributes are OK (which is why A003) is not included here.
"A001",
"A002",
]
This is another benefit of utilizing pyproject.toml
: it allows you to centralize multiple configurations in a single, organized file.
Think of these settings as the safety rails on a mountain road—they guide you correctly and help prevent mishaps before they occur.
The Shift Away from Virtual Environments and Conda
Historically, managing virtual environments or using Conda was an essential part of Python development. However, just as many old-school practices give way to automation, modern package managers now handle environment management for you. They automatically read the Python version specified in your pyproject.toml
and set up the correct environment without manual intervention.
This shift is similar to using a dishwasher instead of washing dishes by hand: it frees you up to focus on writing code rather than spending time on setup tasks.
Here's an example of how easy to use UV to create a new environment and install the dependencies:
# Assuming you just cloned the repository and want to create a new environment with the dependencies to run the project
git clone https://github.com/sirawats/yet-another-fastapi-template
cd yet-another-fastapi-template
uv sync
That's it! You have a new environment with the dependencies to run the project.
If you want to run the project, you can use the following command:
cp .env.example .env
uv run fastapi run src/app/main.py
You don't need to create a new environment by yourself anymore. .venv
will be created in project root automatically.
My Personal Development Stack in 2025
Over time, I've experimented extensively and honed a toolchain that balances modern best practices with ease of use:
- LSP: I rely on Pylance or Pyright with a thoughtfully tuned strict mode. This way, any potential type issues are caught early—kind of like having a second pair of eyes on your code.
- Linter/Formatter: Ruff has become my trusted companion given its combined speed and functionality. It simplifies maintaining consistent code style across the project.
- Package Manager: I've embraced UV for its lightweight design and seamless integration, particularly given that it comes from the same team behind Ruff.
Each component in this stack is like a well-oiled cog, ensuring that you spend more time innovating and less time wrestling with configuration problems.
Bringing It All Together
Modernizing your Python project setup isn't just about following trends—it's about equipping yourself with the right tools for a smoother, more maintainable development process. By centralizing your configuration in pyproject.toml, embracing advanced package managers, and prioritizing type safety, you're setting your project up for long-term success.
While it may seem like a lot to take in at first, remember: you're not expected to master it all immediately. Each step forward is an opportunity to level up your skills and streamline your workflow. Embrace the journey, and soon you'll navigate modern Python development with confidence and ease.
Happy coding, and enjoy exploring the exciting possibilities of Python in 2025!