Usage
Basic command
uv-start <project-name> [options]The project name must not contain spaces or underscores.
Options
| Flag | Description |
|---|---|
-t, --type [lib\|package\|app] |
Project type to create. lib (default) creates a simple library with a src/ layout. package creates an installable package with an entry-point script. app creates a standalone application. |
-p, --python [3.14\|3.13\|3.12\|3.11\|3.10] |
Python version for the new project (default: 3.13). |
-w, --workspace |
Create a uv workspace (monorepo). A shared common-utils library is always created to host shared logging setup; you will be prompted to add additional sub-projects. |
-g, --github |
Initialise a Git repository and create a GitHub remote. Sets up CI/CD workflows automatically. |
--private |
Make the GitHub repository private. Requires --github. |
--data |
Create a data analysis project. Installs Jupyter, pandas, matplotlib, and seaborn. No src/ layout — just a flat project with a starter notebook, lab matplotlib style, and colour palette. |
--napari |
Create a napari plugin project. Installs napari[all] and magicgui, scaffolds a hello-world dock widget, a napari.yaml manifest, and the [project.entry-points."napari.manifest"] block in pyproject.toml. Uses the standard src/ layout and full dev-tool configuration. |
--dotenv |
Add .env handling for environment-driven logging. Installs python-dotenv, ships a .env.example, and uses the env-var-aware logging config (LOG_LEVEL/LOG_FILE/ENV). Omit it for the default loguru config (console only, no .env). |
--config NAME EMAIL |
Save author name and email for project templates. Stored in ~/.config/uv-start/config.toml. |
Examples
Create a library (default)
uv-start my-libCreates a my-lib/ directory with a src/my_lib/ package, test directory, and all dev-tool configuration.
Create an installable package
uv-start my-cli -t packageSame as a library but also registers a console script entry-point so the package can be run from the command line.
Specify a Python version
uv-start my-project -p 3.12Target Python 3.12 instead of the default 3.13.
Create a project with a GitHub repo
uv-start my-project -gInitialises a local Git repo, creates a public GitHub remote, pushes an initial commit, and sets up CI workflows.
uv-start my-project -g --privateSame as above but the GitHub repository is private.
Create a workspace (monorepo)
uv-start my-workspace -wSets up a uv workspace. A common-utils shared library is always created to host the shared loguru logging setup. You will then be prompted to add additional sub-projects to the workspace.
Combine workspace and GitHub:
uv-start my-workspace -w -gCreate a data analysis project
uv-start my-analysis --dataCreates a flat project (no src/ layout) pre-configured for interactive data analysis:
- Jupyter, pandas, matplotlib, and seaborn installed in the
.venv - Lab matplotlib style (
hhlab_style01.mplstyle) at the project root colors.py— aCOLORenum with the lab’s standard palettesample.ipynb— starter notebook that applies the style and importsCOLORCLAUDE.md— instructions for AI-assisted work that enforce consistent figure styling
Combine with --github to create a GitHub repository at the same time:
uv-start my-analysis --data -gCreate a napari plugin project
uv-start my-plugin --napariCreates a standard src/my_plugin/ package pre-wired as a napari plugin:
- napari[all] and magicgui installed as runtime dependencies
src/my_plugin/napari.yaml— plugin manifest with one widget contributionsrc/my_plugin/_widget.py— hello-worldmagic_factorydock widget[project.entry-points."napari.manifest"]block appended topyproject.tomlso the manifest is discovered when the package is installed- Full dev-tool stack (ruff, ty, pytest, commitizen, pre-commit)
After scaffolding, run the widget end-to-end with:
cd my-plugin
uv sync
uv run napariThen in napari, open Plugins → my-plugin → Hello Widget to see the Say hello button.
Combine with --github to publish the plugin repository at the same time:
uv-start my-plugin --napari -gGenerated project structure
Standard project
project-name/
├── src/
│ └── project_name/
│ ├── __init__.py
│ └── config.py ← loguru logging setup
├── tests/
├── pyproject.toml
├── README.md
├── LICENSE
├── .env.example ← only with --dotenv
└── .pre-commit-config.yaml
Workspace
workspace-name/
├── packages/
│ ├── common_utils/ ← shared logging setup (always created)
│ └── package1/
├── pyproject.toml
├── README.md
└── .pre-commit-config.yaml
Data analysis project
analysis-name/
├── hhlab_style01.mplstyle ← lab matplotlib style, loaded by the notebook
├── colors.py ← COLOR enum with the lab palette
├── sample.ipynb ← starter notebook
├── CLAUDE.md ← AI instructions for consistent figure style
├── pyproject.toml
├── README.md
├── .env.example ← only with --dotenv
└── .gitignore
napari plugin project
plugin-name/
├── src/
│ └── plugin_name/
│ ├── __init__.py
│ ├── config.py
│ ├── napari.yaml ← plugin manifest
│ └── _widget.py ← hello-world magicgui widget
├── tests/
├── pyproject.toml ← includes [project.entry-points."napari.manifest"]
├── README.md
├── LICENSE
├── .env.example ← only with --dotenv
└── .pre-commit-config.yaml
Development tools configured
Every generated project comes with:
- Ruff — linter and formatter (line length 79, flake8 + isort rules)
- Ty — static type checker (checks
src/andtests/) - pytest — test framework with automatic discovery in
tests/ - commitizen — conventional commits, automatic version bumping, and changelog generation (see Conventional commits and version bumps below)
- pre-commit — Git hooks that run linting, formatting, and type checking before each commit
- loguru — logging with a colourised console sink by default and an opt-in rotating file sink (env-driven configuration via
--dotenv; see Logging system below)
Data analysis project details
Matplotlib style
hhlab_style01.mplstyle is placed at the project root so it can be referenced by name from any notebook in the same directory:
import matplotlib.pyplot as plt
plt.style.use('hhlab_style01.mplstyle')Always apply this style before creating any figure.
Lab colour palette
colors.py defines the COLOR enum with the lab’s standard palette. Use the enum values — never raw hex strings or default matplotlib colors:
from colors import COLOR
ax.scatter(x, y, color=COLOR.BLUE.value)
ax.bar(labels, heights, color=COLOR.PINK.value)Available colours:
| Name | Hex | Typical use |
|---|---|---|
COLOR.BLUE |
#526C94 |
Primary data series |
COLOR.LIGHT_BLUE |
#75B1CE |
Secondary / paired condition |
COLOR.PINK |
#DC6B83 |
Contrast / highlight |
COLOR.YELLOW |
#D8C367 |
Third condition |
COLOR.TURQUOISE |
#00bfb2 |
Fourth condition |
COLOR.LIGHT_GREEN |
#CCDBA2 |
Fifth condition |
COLOR.LAVENDER |
#C6B2D1 |
Sixth condition |
COLOR.PURPLE |
#654875 |
Seventh condition |
COLOR.OLIVE |
#889466 |
Eighth condition |
COLOR.GREY |
#D4D3CF |
Background / negative control |
COLOR.DARKGREY |
#4A4A4A |
Text / annotations |
To add a new colour, extend the COLOR enum in colors.py rather than scattering raw hex values through notebooks.
Starter notebook
sample.ipynb opens with the standard imports already in place:
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
from colors import COLOR
plt.style.use('hhlab_style01.mplstyle')It also includes a commented example scatter plot to illustrate the expected usage pattern.
CLAUDE.md
The generated CLAUDE.md tells Claude Code (and other AI tools) to always apply the lab style and use the COLOR enum. This keeps AI-generated figure code consistent with hand-written code across the project.
Conventional commits and version bumps
Generated projects use commitizen to enforce conventional commits and automate semantic versioning.
Use cz commit instead of git commit to get an interactive prompt that builds a correctly formatted message:
cz commitTo bump the version based on your commit history:
cz bumpCommitizen inspects all commits since the last tag and determines the version bump automatically:
| Commit prefix | Version bump | Example |
|---|---|---|
fix: |
PATCH (0.0.X) | fix: handle missing config file |
feat: |
MINOR (0.X.0) | feat: add --config flag |
BREAKING CHANGE: in footer, or ! after type |
MAJOR (X.0.0) | feat!: replace .env with config system |
Other commit types — chore:, docs:, refactor:, ci:, test:, style:, perf:, build: — are recorded in the changelog but do not trigger a version bump.
While major_version_zero is enabled in the commitizen configuration (the default for generated projects), breaking changes bump the minor version instead of major, following the SemVer spec for 0.x releases.
Workspace versioning
When a workspace is created with -w, all packages share a single synchronized version. The commitizen configuration lives only in the root pyproject.toml and its version_files list covers every sub-package:
src/<module>/__init__.py—__version__stringpyproject.toml—versionfieldREADME.md— version badge
Running cz bump at the workspace root updates all of these files across the root project and every sub-package in one step. The release CI workflow does the same automatically when a conventional commit lands on main.
GitHub CI/CD workflows
When --github is used, two GitHub Actions workflows are created:
CI pipeline — runs on every push and pull request:
- Ruff linting and format checking
- Ty type checking
- pytest test suite
Release pipeline — runs on pushes to main:
- Automatic version bumping based on conventional commits
- GitHub release creation with changelog
- For workspaces, bumps the single synchronized version across all packages
Logging system
Every generated project includes a ready-to-use logging module at src/<package_name>/config.py, built on loguru. Importing the package configures loguru’s sinks once (the package __init__ does from . import config), after which you use the global logger anywhere.
Getting a logger
There is no per-module factory — import loguru’s global logger directly:
from loguru import logger
logger.debug("Detailed diagnostic info")
logger.info("General operational messages")
logger.warning("Something unexpected happened")
logger.error("Something failed")The module path, function and line are captured automatically, and tracebacks are rendered with full diagnostics.
Default configuration
Out of the box, config.py enables a single colourised console sink at INFO:
import sys
from loguru import logger
logger.remove()
logger.add(sys.stderr, level="INFO", colorize=True)A rotating file sink is included but commented out. Uncomment it to also write logs to disk — loguru handles rotation, retention and compression for you:
logger.add(
"logs/app.log",
level="DEBUG",
rotation="1 MB",
retention=5,
compression="zip",
backtrace=True,
diagnose=True,
)Workspace logging
In a workspace, logging is centralised in common-utils at the bottom of the dependency graph. Only application entry points (main()) call setup_logging_once(); library packages only emit log messages.
# In an app member's __init__.py
from common_utils.logging_setup import setup_logging_once
from loguru import logger
def main() -> None:
setup_logging_once()
logger.info("Hello from my-app!")Environment-driven configuration (--dotenv)
If you scaffold with --dotenv, the project instead gets a config.py that reads its configuration from environment variables (optionally loaded from .env / .env.<ENV> via python-dotenv), plus a .env.example to copy:
uv-start my-project --dotenv
cp .env.example .envThe .env file is gitignored by default. Never commit it — it is for local configuration and secrets only.
Recognised variables (all optional):
| Variable | Default | Description |
|---|---|---|
LOG_LEVEL |
INFO |
Minimum level for the console (and file) sink (DEBUG, INFO, WARNING, ERROR, CRITICAL) |
LOG_FILE |
(unset) | If set, also write a rotating log file to this path |
ENV |
development |
Active environment; loads .env.<ENV> overrides if present |
Environment-specific configuration
With --dotenv, create environment-specific files to override defaults:
.env.example— template with defaults (checked into version control).env— your local copy of.env.example(gitignored, never commit).env.production— production settings (gitignored)
Example .env.production:
LOG_LEVEL=WARNING
LOG_FILE=/var/log/myapp/app.logActivate an environment:
export ENV=productionThe loading strategy is:
- Load the base
.envfile (if present) - Load
.env.<ENV>(if present), overriding previous values - Fall back to code-level defaults (console at
INFO) for anything unset