An Opinionated Guide to Python Projects
Updated on: 2026-10-18.
This post is meant to be an opinionated guide to setting up and developing a Python project. The goal is to give a paved path that avoids common snags.
Let uv manage Python
Python project management has become drastically simpler with the introduction of uv (which consolidated many tools and made everything way faster). There are a few main things it will handle for you: setting up a project structure, installing Python, and installing the dependencies.
Much of your use of uv can reduce down to simply using uv run to execute everything.
1# Run a script
2uv run file.py
3# Run tests
4uv run pytest
5# Start a Python shell
6uv run python
7# Run a bash script with the Python environment active
8uv run ./mybash.sh
(You can use alias ur="uv run" to type less, but I find I generally don’t need it.)
This all works because uv run X will automatically:
- create a
.venvdirectory with a bare virtual environment and ensure you have an isolated Python that matches your project’s requirements (similar to callinguv venv)1 - install your project dependencies in the
.venvenvironment (similar to callinguv sync) - execute
Xin an environment where the packages from.venvare available and so are the additional programs in.venv/bin
This takes care of all Python and environment management issues (as long as your project is configured correctly).2
Use uv init to get a standard project layout
If you’re starting a brand new project, uv init will create the boilerplate for you, along with a template pyproject.toml.
1[project]
2name = "package"
3version = "0.1.0"
4description = "Add your description here"
5readme = "README.md"
6authors = [
7 { name = "Dmitry Shemetov", email = "..." }
8]
9requires-python = ">=3.15"
10dependencies = []
11
12[project.scripts]
13package = "package:main"
14
15[build-system]
16requires = ["uv_build>=0.12.17,<0.13.0"]
17build-backend = "uv_build"
This is the layout the above command prepares:3
1.
2├── pyproject.toml
3├── README.md
4└── src
5 └── package
6 └── __init__.py
With these contents:
1# __init__.py
2def main() -> None:
3 print("Hello from package!")
As your project develops, you will probably want to add:
- a
tests/top-level directory with files liketest_module.pywhichpytestwill automatically collect and run - a
docs/directory for documentation - a
LICENSEfile
Use main as your program entry point
(If your project is a library and doesn’t need an entry point, you can use uv init --lib or simply delete the [project.scripts] section in pyproject.toml.)
How do we actually call our program though? We need an entry point. The sample project above defines it in [project.scripts] in pyproject.toml to be the main function, so
1❯ uv run package
2Hello from package!
This is preferable to executing files directly with uv run python script.py or uv run file.py because the command doesn’t depend on file paths (and relative imports can break when files are run directly).
Generally, it’s handy to have some ways to customize your program’s behaviors. This is where typer can help you to quickly make a CLI by taking much of the boilerplate out of the process of capturing arguments.
Use absolute imports
When importing sibling modules into a module, you can either use relative or absolute imports. My preference is to stick to absolute imports, because this consistency makes repo-wide import searches comprehensive.
1# absolute import in package/module.py
2from package.subpackageA import moduleA
3
4# absolute import in package/subpackageA/moduleA.py
5from package import module
If imports are giving you troubles, some helpful commands for debugging:
1# Print the actual copy of the package imported.
2uv run python -c 'import package; print(package.__file__)'
3# Print the directories Python searches for imports, in order.
4uv run python -c 'import sys; print(*sys.path, sep="\n")'
Standard dependencies
Some useful tools I use in most projects:
ruffis the standard code formatter and linterpytestis the standard testing frameworktyperis a very handy CLI framework
1dependencies = [
2 "typer>=0.27.3",
3]
4
5[dependency-groups]
6dev = [
7 "pytest>=9.1.1",
8 "ruff>=0.17.1",
9]
Along with my standard ruff settings:
1[tool.ruff]
2line-length = 120
3lint.extend-select = [
4 "I", # isort
5 "NPY", # numpy
6]
7format.docstring-code-format = true
Further reading
These are handy references if you ever need to go deeper:
- Writing your pyproject.toml - Python Packaging User Guide
- Working on projects | uv
- Good Integration Practices - pytest documentation
- Modules - The Python Tutorial, modules vs packages distinction and terminology
- Dependency groups and uv run | Simon Willison’s TILs
Footnotes
If you’re entering an existing project,
uvwill read either.python-versionorpyproject.tomlto understand which version of Python your project needs (or else default to something recent). You can also see which Pythonsuvhas installed on your system withuv python list. ↩︎If you’re working on an older Python project,
uv syncmight not work directly. You might need to douv init --bare(to make sure you have apyproject.toml), thenuv add -r requirements.txtto install and record inuv.lock. ↩︎We just use
uv’s default. The benefits of thesrc/layout over the “flat” layout are relatively minor - see here. ↩︎