app convention
pyr projects follow a convention. It's opinionated on purpose.
myapp/
pyproject.toml
requirements.txt
.gitignore
app/
__init__.py
config.py
main.py
.venv/This isn't a suggestion. When you run pyr init, this is what you get. When you
run pyr run, it executes app/main.py with PYTHONPATH=. set. That one
environment variable is the whole trick.
Why this matters
The most common question from someone starting a Python project is some variation of: "I have two files and I can't import one from the other."
The answers they find are:
- "Add
sys.path.append('..')to your script" (fragile, breaks when you move files) - "Use relative imports with dots" (confusing, fails when you run the file directly)
- "Install your project in editable mode with
pip install -e ." (requires a full packaging setup for a script) - "Just put everything in one file" (works until it doesn't)
None of these are wrong. All of them are ceremony. The actual answer is: put
your code in a package directory with an __init__.py, and make sure the
project root is on PYTHONPATH. Then from app.config import ENV works
everywhere, every time, whether you're in main.py, a test file, or a one-off
script.
pyr run does this for you. No configuration. No editable installs. No path
hacking.
The files
app/main.py
def main():
print("hello world")
if __name__ == "__main__":
main()This is the entrypoint. pyr run executes this file. The main() function
wrapper with the __name__ guard means the file works both as a module
(from app.main import main) and as a standalone script.
app/config.py
import os
ENV = os.getenv("ENV", "development")Configuration lives here. Environment variables, feature flags, constants.
Import it anywhere in your project with from app.config import ENV. One file,
one source of truth.
This is deliberately minimal. It's not a settings framework. It's os.getenv
with a default. You add what you need as you need it.
app/init.py
Empty. That's fine. Its job is to make app/ a Python package so imports
resolve. You can add package-level exports later if you want, but you don't need
to.
Growing the project
The convention scales. Add modules as your project grows:
app/
__init__.py
config.py
main.py
db.py
models.py
routes.py
utils.pyEvery file can import from every other file:
# in app/routes.py
from app.config import ENV
from app.db import get_connection
from app.models import UserNo relative imports. No path manipulation. Just
from app.whatever import thing.
Subdirectories
When a module gets big enough to split, make it a package:
app/
__init__.py
config.py
main.py
services/
__init__.py
auth.py
email.py# in app/main.py
from app.services.auth import authenticate
from app.services.email import send_welcomeSame pattern. The __init__.py in the subdirectory makes it a package. Imports
stay clean and absolute.
Tests
Put tests next to the code or in a top-level tests/ directory. Both work
because PYTHONPATH=. is set at the project root.
myapp/
app/
__init__.py
config.py
main.py
tests/
__init__.py
test_main.py# in tests/test_main.py
from app.main import main
from app.config import ENVRun them however you like — pyr run -- -m pytest, or directly with the venv's
Python. The imports work because the project root is on the path.
Passing arguments
pyr run forwards arguments after -- to Python:
pyr run -- --verbose
pyr run -- -m pytest
pyr run -- -m pytest tests/ -vThe -- separates pyr's flags from Python's. Everything after it goes straight
through.
Why not src layout?
The Python packaging world has a long debate about src/ layout vs flat layout.
The src/ layout puts your package inside a src/ directory to prevent
accidental imports from the project root during development. It solves a real
problem — but it solves it for library authors distributing packages via PyPI.
pyr is for applications. You're building something that runs, not something that
gets installed into other people's environments. The flat app/ layout is
simpler, the imports are cleaner, and the failure mode it prevents (accidentally
importing uninstalled code) doesn't apply when pyr run controls the
environment.
If you're building a library for distribution, pyr is probably not the right
tool. Use uv or poetry or flit. If you're building something that runs — a web
app, a CLI, a data pipeline, a script that grew up — the app/ convention is
for you.
The point
The convention is the product. Any tool can create a venv and install packages. pyr gives you a project structure that works from the first file to the hundredth, with imports that never break, and a runner that makes it invisible.
You don't configure it. You don't think about it. You write
from app.whatever import thing and it works.