A minimal Python project that prints a greeting, accepts a name from the command line, includes a pytest test suite, and uses a Makefile to drive common tasks. This tutorial covers project setup with uv, argument parsing, testing with pytest, and automation for a simple but complete workflow.
Prerequisites
- Python 3.10 or later
- uv 0.4 or later (
curl -LsSf https://astral.sh/uv/install.sh | sh) - A Unix-like terminal (macOS, Linux, or WSL on Windows)
makeinstalled (pre-installed on macOS and most Linux distributions)
Step 1: Initialize the project with uv
Create the file
uv init hello-world
cd hello-world
Detailed breakdown
uv init hello-worldscaffolds a new Python project with apyproject.toml, amain.pysample file, and a.python-versionfile.pyproject.tomlserves as the project manifest, replacing the need forsetup.pyorrequirements.txt.- uv automatically creates a
.venvvirtual environment on first run.
Step 2: Add pytest and set up the project
Create the file
Remove the generated sample file and add pytest as a dev dependency:
rm main.py README.md
uv add --dev pytest
Detailed breakdown
rm main.py README.mdremoves the placeholder files generated byuv initsince the project will use its own module structure.uv add --dev pytestinstalls pytest and records it under[dependency-groups]inpyproject.toml. Dev dependencies are only needed during development and testing.
Step 3: Set up the project structure
Create the file
touch .gitignore
Add the code: .gitignore
__pycache__/
*.pyc
.venv/
.pytest_cache/
.DS_Store
*.log
tmp/
Detailed breakdown
__pycache__/and*.pycexclude Python bytecode files generated at runtime..venv/excludes the virtual environment that uv manages locally..pytest_cache/excludes pytest cache directories..DS_Storeandtmp/cover common OS and temporary artifacts.
Step 4: Create the greeter module
This module contains the core greeting logic, separated from the entry point so it can be tested independently.
Create the file
mkdir -p src
touch src/__init__.py
touch src/greeter.py
Add the code: src/greeter.py
"""Core greeting logic."""
def greet(name: str = "World") -> str:
"""Return a greeting string for the given name.
Args:
name: The name to greet. Defaults to "World".
Returns:
A formatted greeting string.
"""
return f"Hello, {name}!"
Detailed breakdown
greetaccepts an optionalnameparameter and defaults to"World"when no name is provided.- The function returns a string rather than printing directly. This makes it easy to test the return value without capturing stdout.
- The module lives under
src/to keep application code separate from tests and project-root files.
Step 5: Create the command-line entry point
The entry point parses an optional --name argument and prints the greeting.
Create the file
touch src/main.py
Add the code: src/main.py
"""Command-line entry point for the hello-world application."""
import argparse
from src.greeter import greet
def main() -> None:
"""Parse arguments and print a greeting."""
parser = argparse.ArgumentParser(description="Print a greeting.")
parser.add_argument(
"--name",
default="World",
help="Name to greet (default: World)",
)
args = parser.parse_args()
print(greet(args.name))
if __name__ == "__main__":
main()
Detailed breakdown
argparseis used instead of readingsys.argvdirectly. This gives automatic--helpsupport and clean error messages for invalid arguments.- The
--nameflag defaults to"World", matching thegreetfunction default, so running with no arguments producesHello, World!. - The
if __name__ == "__main__"guard ensuresmain()only runs when the file is executed directly. - The import
from src.greeter import greetassumes execution from the project root directory.
Step 6: Create the test suite
Tests use pytest to verify both the default greeting and a custom name greeting.
Create the file
mkdir -p tests
touch tests/__init__.py
touch tests/test_greeter.py
Add the code: tests/test_greeter.py
"""Tests for the greeter module."""
from src.greeter import greet
def test_default_greeting() -> None:
"""greet() with no arguments returns 'Hello, World!'."""
assert greet() == "Hello, World!"
def test_custom_name() -> None:
"""greet('Python') returns 'Hello, Python!'."""
assert greet("Python") == "Hello, Python!"
def test_empty_string() -> None:
"""greet('') returns 'Hello, !'."""
assert greet("") == "Hello, !"
Detailed breakdown
- Tests use plain functions with
assertstatements instead ofunittest.TestCaseclasses. This is the idiomatic pytest style — simpler and less boilerplate. test_default_greetingverifies the default parameter path.test_custom_nameverifies that a supplied name appears in the output.test_empty_stringdocuments the edge case behavior when an empty string is passed.- Each function name starts with
test_so pytest discovers them automatically.
Step 7: Create the Makefile
The Makefile provides a single command interface for running, testing, and cleaning the project. Running make with no arguments prints a help screen.
Create the file
touch Makefile
Add the code: Makefile
.DEFAULT_GOAL := help
.PHONY: help run test clean
help: ## Show this help screen
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " %-15s %s\n", $$1, $$2}'
run: ## Run the application (use NAME= to set a custom name)
uv run python -m src.main $(if $(NAME),--name "$(NAME)",)
test: ## Run the test suite
uv run pytest tests/ -v
clean: ## Remove bytecode and cache files
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
find . -type d -name .pytest_cache -exec rm -rf {} + 2>/dev/null || true
find . -name '*.pyc' -delete 2>/dev/null || true
Detailed breakdown
.DEFAULT_GOAL := helpensures that running plainmakeprints the help screen rather than executing a build target.- The
helptarget usesgrepandawkto extract targets annotated with##comments and format them into a readable list. runusesuv run python -m src.mainto execute the module inside the uv-managed virtual environment. The optionalNAMEvariable lets users runmake run NAME=Python.testusesuv run pytest tests/ -vto run all test files undertests/with verbose output.cleanremoves__pycache__,.pytest_cache, and.pycfiles. The|| truepreventsmakefrom failing if no matches are found.
Step 8: Run and validate
Execute the following commands from the hello-world/ directory.
Verify the help screen
cd hello-world && make
Expected output:
help Show this help screen
run Run the application (use NAME= to set a custom name)
test Run the test suite
clean Remove bytecode and cache files
Run the application
make run
Expected output:
Hello, World!
Run with a custom name
make run NAME=Python
Expected output:
Hello, Python!
Run the tests
make test
Expected output:
tests/test_greeter.py::test_default_greeting PASSED
tests/test_greeter.py::test_custom_name PASSED
tests/test_greeter.py::test_empty_string PASSED
Troubleshooting
ModuleNotFoundError: No module named 'src': Make sure you run commands from thehello-world/project root, not from insidesrc/ortests/.make: *** No rule to make target ...: Verify the Makefile uses tabs for indentation, not spaces. Many editors convert tabs to spaces by default.uv: command not found: Install uv withcurl -LsSf https://astral.sh/uv/install.sh | shand restart your shell.
Recap
This tutorial built a complete Python hello-world project with:
- A greeter module with a testable
greetfunction - A command-line entry point using
argparse - A pytest test suite with three test cases in a
tests/directory - A Makefile with a default help screen, run, test, and clean targets
- Project managed with uv for dependency management and script execution
Next improvements could include adding type checking with mypy, a formatter like ruff, or adding a script entry point in pyproject.toml.