# project setup consistent structure across projects: src/ layout, pyproject.toml as single source of truth, justfile for commands. ## directory structure ``` myproject/ ├── src/myproject/ │ ├── __init__.py │ ├── cli.py │ ├── settings.py │ └── _internal/ # private implementation ├── tests/ ├── pyproject.toml ├── justfile └── .pre-commit-config.yaml ``` the `src/` layout prevents accidental imports from the working directory. your package is only importable when properly installed. ## pyproject.toml ```toml [project] name = "myproject" description = "what it does" readme = "README.md" requires-python = ">=3.10" dynamic = ["version"] dependencies = [ "httpx>=0.27", "pydantic>=2.0", ] [project.scripts] myproject = "myproject.cli:main" [build-system] requires = ["hatchling", "uv-dynamic-versioning"] build-backend = "hatchling.build" [tool.hatch.version] source = "uv-dynamic-versioning" [tool.uv-dynamic-versioning] vcs = "git" style = "pep440" bump = true fallback-version = "0.0.0" [dependency-groups] dev = [ "pytest>=8.0", "ruff>=0.8", "ty>=0.0.1a6", ] ``` key patterns: - `dynamic = ["version"]` - version comes from git tags, not manual editing - `[project.scripts]` - CLI entry points ## dependency groups vs optional dependencies these look similar but serve different purposes. **dependency groups** (PEP 735) are local-only. they never appear in published package metadata. users who `pip install` your package won't see them: ```toml [dependency-groups] dev = ["pytest", "ruff"] docs = ["mkdocs", "mkdocs-material"] ``` install with `uv sync --group dev`. CI can install only what it needs. **optional dependencies** are published in package metadata. users can install them: ```toml [project.optional-dependencies] aws = ["prefect-aws"] mcp = ["fastmcp>=2.0"] ``` install with `pip install mypackage[aws]` or `uv add 'mypackage[mcp]'`. use groups for dev/test/CI. use optional deps for features consumers might want. from [switching a big python library from setup.py to pyproject.toml](https://blog.zzstoatzz.io/switching-a-big-python-library-from-setuppy-to-pyprojecttoml/) ## versioning from git tags with `uv-dynamic-versioning`, your version is derived from git: ```bash git tag v0.1.0 git push --tags ``` no more editing `__version__` or `pyproject.toml` for releases. ## justfile ```makefile check-uv: #!/usr/bin/env sh if ! command -v uv >/dev/null 2>&1; then echo "uv is not installed. Install: curl -LsSf https://astral.sh/uv/install.sh | sh" exit 1 fi install: check-uv uv sync test: uv run pytest tests/ -xvs lint: uv run ruff format src/ tests/ uv run ruff check src/ tests/ --fix check: uv run ty check ``` run with `just test`, `just lint`, etc. ## multiple entry points for projects with both CLI and MCP server: ```toml [project.scripts] myproject = "myproject.cli:main" myproject-mcp = "myproject.mcp:main" ``` ## uv workspaces for multi-package repos (like plyr-python-client): ``` myproject/ ├── packages/ │ ├── core/ │ │ ├── src/core/ │ │ └── pyproject.toml │ └── mcp/ │ ├── src/mcp/ │ └── pyproject.toml ├── pyproject.toml # root workspace config └── uv.lock ``` root pyproject.toml: ```toml [tool.uv.workspace] members = ["packages/*"] [tool.uv.sources] core = { workspace = true } mcp = { workspace = true } ``` packages can depend on each other. one lockfile for the whole workspace. ## simpler build backend for projects that don't need dynamic versioning, `uv_build` is lighter: ```toml [build-system] requires = ["uv_build>=0.9.2,<0.10.0"] build-backend = "uv_build" ``` sources: - [pdsx/pyproject.toml](https://github.com/zzstoatzz/pdsx/blob/main/pyproject.toml) - [plyr-python-client](https://github.com/zzstoatzz/plyr-python-client)