Skip to main content

Poe the Poet: Your Python Project's Personal Task Manager

Calculating read time…

Imagine having a smart assistant for your Python project. You tell them "run tests" and they know exactly what to do, every single time.

No remembering long commands. No typing the same thing repeatedly. No confusion about which virtual environment to use.




What is Poe the Poet? The Simple Answer 🎯

Poe the Poet is a task runner for Python projects.

Think of it as saving shortcuts on your phone. Instead of typing "Hey Google, play my workout playlist on Spotify," you just say "start workout" and it does everything automatically.

Poe lets you save common project tasks (testing, linting, building) as simple shortcuts that run with one command.

💡 Real-World Analogy:

Without Poe the Poet: "Computer, open terminal, activate virtual environment, run pytest with coverage flags, then run black formatter with specific settings..."

With Poe the Poet: "poe test" → Everything runs automatically! ✨

Why Do We Need Poe? The Problem It Solves ?

The Manual Chaos (Before Poe)

Imagine you're working on a Python web application. Every day, you need to run these commands:

# Activate virtual environment
source venv/bin/activate  # or: poetry shell

# Run tests with coverage
pytest tests/ --cov=myapp --cov-report=html --verbose

# Check code style
black myapp/ --line-length=88 --check

# Lint for errors
pylint myapp/ --rcfile=.pylintrc

# Check type hints
mypy myapp/ --strict --ignore-missing-imports

# Start development server
python -m uvicorn myapp.main:app --reload --port 8000

Problems with this:

  • Long commands that are hard to remember
  • Easy to make typos
  • Teammates might run things differently
  • Forget to activate virtual environment
  • Copy-paste from notes or chat history
  • New team members confused about what to run

The Poe Way (Modern Approach)

Define tasks once in your pyproject.toml:

[tool.poe.tasks]
test = "pytest tests/ --cov=myapp --cov-report=html --verbose"
format = "black myapp/ --line-length=88"
lint = "pylint myapp/ --rcfile=.pylintrc"
typecheck = "mypy myapp/ --strict --ignore-missing-imports"
serve = "python -m uvicorn myapp.main:app --reload --port 8000"

Now run them with simple commands:

poe test       # Runs tests
poe format     # Formats code
poe lint       # Checks code quality
poe typecheck  # Validates types
poe serve      # Starts server
✅ Benefits:
  • Short, memorable commands
  • Consistent across all team members
  • Automatically uses correct virtual environment
  • Self-documenting (tasks listed in config)
  • Easy for newcomers to understand

Installing Poe the Poet 🛠️

Prerequisites

You need Python 3.10 or higher:

python --version
# Should show: Python 3.10.x or higher

Installation Option 1: With pipx (Recommended)

Pipx installs Poe globally, making it available for all your projects:

# Install pipx if you don't have it
python -m pip install pipx
python -m pipx ensurepath

# Install Poe the Poet globally
pipx install poethepoet

# Verify installation
poe --version
# Output: Poe the Poet - version 0.40.0

Installation Option 2: As Poetry Plugin

If you're using Poetry, you can install Poe as a plugin:

# Install as Poetry plugin
poetry self add 'poethepoet[poetry-plugin]'

# Now you can use 'poetry poe' command
poetry poe --version

Installation Option 3: As Development Dependency

Install Poe as part of your project:

# With Poetry
poetry add --group dev poethepoet

# With uv
uv add --dev poethepoet

# With pip
pip install poethepoet
💡 Which Method Should You Choose?
  • Use pipx: If you want Poe available everywhere (best for most people)
  • Use Poetry plugin: If you exclusively use Poetry and want tight integration
  • Use project dependency: If you want Poe version-locked with your project

Understanding Task Types 📚

Poe supports four types of tasks. Let's explore each with practical examples!

Type 1: Command Tasks (Most Common)

Simple shell commands, exactly as you'd type them:

[tool.poe.tasks]
# Single command
test = "pytest tests/"

# Command with multiple options
test-verbose = "pytest tests/ --verbose --cov=myapp"

# Multiple tools can have separate tasks
format = "black ."
lint = "ruff check ."

Running command tasks:

poe test
# Output: Poe => pytest tests/
# (Poe shows you what command it's running)

poe test-verbose
# Runs: pytest tests/ --verbose --cov=myapp

Type 2: Script Tasks (Python Functions)

Run Python functions directly from your code:

[tool.poe.tasks]
# Format: "module.submodule:function_name(arguments)"
serve.script = "myapp.server:run(debug=True)"
seed-db.script = "myapp.database:seed()"

This runs the run() function from myapp/server.py:

# In myapp/server.py
def run(debug=False):
    """Start the development server"""
    import uvicorn
    uvicorn.run(
        "myapp.main:app", 
        reload=debug, 
        port=8000
    )

When to use script tasks:

  • Need to run Python code directly
  • Want to pass complex arguments
  • Need programmatic control

Type 3: Shell Tasks (Multi-Command Scripts)

Run multiple commands in sequence:

[tool.poe.tasks.setup]
shell = """
    echo "Setting up project..."
    pip install -r requirements.txt
    python manage.py migrate
    python manage.py createsuperuser
    echo "Setup complete!"
"""

[tool.poe.tasks.deploy]
shell = """
    echo "Building application..."
    docker build -t myapp .
    docker push myapp:latest
    kubectl apply -f k8s/
"""

When to use shell tasks:

  • Need to run multiple commands together
  • Want to use shell features (pipes, redirects)
  • Setting up complex environments
⚠️ Platform Warning:

Shell tasks use your system's shell (bash/zsh on Mac/Linux, cmd/PowerShell on Windows). Commands might not work cross-platform!

For cross-platform projects, prefer command tasks or script tasks.

Type 4: Sequence Tasks (Running Multiple Tasks)

Chain multiple Poe tasks together:

[tool.poe.tasks]
# Define individual tasks
format = "black ."
lint = "ruff check ."
typecheck = "mypy src/"
test = "pytest tests/"

# Combine them into a quality check
check.sequence = ["format", "lint", "typecheck", "test"]
check.ignore_fail = "return_non_zero"  # Continue even if one fails

Now run all checks with one command:

poe check
# Runs: format, then lint, then typecheck, then test
# Shows progress for each step

Sequence task options:

  • ignore_fail = "return_non_zero" - Continue if task fails
  • ignore_fail = "return_zero" - Stop on first failure (default)

Creating Your First Poe Project 🎨

Let's build a complete example from scratch!

Step 1: Create Project Structure

# Create project directory
mkdir weather-cli
cd weather-cli

# Create basic structure
mkdir src tests
touch src/weather.py
touch tests/test_weather.py
touch pyproject.toml

Step 2: Setup pyproject.toml

[project]
name = "weather-cli"
version = "0.1.0"
requires-python = ">=3.10"

[project.dependencies]
requests = ">=2.31.0"
rich = ">=13.0.0"

[project.optional-dependencies]
dev = [
    "pytest>=8.0.0",
    "pytest-cov>=4.0.0",
    "black>=24.0.0",
    "ruff>=0.3.0"
]

[tool.poe.tasks]
# Simple command tasks
test = "pytest tests/ --verbose"
test-cov = "pytest tests/ --cov=src --cov-report=html"
format = "black src/ tests/"
format-check = "black src/ tests/ --check"
lint = "ruff check src/ tests/"

# Script task to run the app
run.script = "src.weather:main()"

# Shell task for setup
setup.shell = """
    echo "Installing dependencies..."
    pip install -e .[dev]
    echo "Setup complete! Run 'poe test' to verify."
"""

# Sequence task for CI
ci.sequence = ["format-check", "lint", "test-cov"]
ci.ignore_fail = "return_non_zero"

Step 3: Create the Application

In src/weather.py:

import requests
from rich.console import Console

console = Console()

def get_weather(city: str) -> dict:
    """Fetch weather for a city"""
    url = f"https://wttr.in/{city}?format=j1"
    response = requests.get(url)
    return response.json()

def main():
    """Main CLI entry point"""
    console.print("[bold blue]Weather CLI[/bold blue]")
    city = input("Enter city name: ")
    
    with console.status(f"Fetching weather for {city}..."):
        weather = get_weather(city)
    
    temp = weather["current_condition"][0]["temp_C"]
    desc = weather["current_condition"][0]["weatherDesc"][0]["value"]
    
    console.print(f"\n[green]Temperature:[/green] {temp}°C")
    console.print(f"[green]Conditions:[/green] {desc}")

if __name__ == "__main__":
    main()

Step 4: Create Tests

In tests/test_weather.py:

from src.weather import get_weather

def test_get_weather():
    """Test weather fetching"""
    result = get_weather("London")
    assert "current_condition" in result
    assert len(result["current_condition"]) > 0

Step 5: Use Your Tasks!

# Install everything
poe setup

# Run the application
poe run

# Run tests
poe test

# Format code
poe format

# Run all CI checks
poe ci

Perfect! You now have a fully automated project workflow! 🎉

Advanced Task Features 🚀

Adding Arguments to Tasks

Make tasks accept dynamic input:

[tool.poe.tasks.test]
cmd = "pytest ${path} --verbose"
args = [
    {name = "path", default = "tests/", positional = true}
]

[tool.poe.tasks.serve]
cmd = "uvicorn myapp.main:app --port ${port}"
args = [
    {name = "port", default = "8000", options = ["--port", "-p"]}
]

Using tasks with arguments:

# Use default path
poe test
# Runs: pytest tests/ --verbose

# Specify custom path
poe test tests/unit
# Runs: pytest tests/unit --verbose

# Use default port
poe serve
# Runs: uvicorn myapp.main:app --port 8000

# Custom port
poe serve --port 3000
# Runs: uvicorn myapp.main:app --port 3000

Task Help Documentation

Add helpful descriptions to your tasks:

[tool.poe.tasks.test]
help = "Run the test suite with coverage reporting"
cmd = "pytest tests/ --cov=src"

[tool.poe.tasks.deploy]
help = "Deploy application to production"
shell = """
    docker build -t myapp .
    kubectl apply -f k8s/
"""

View all available tasks:

poe --help

# Output:
CONFIGURED TASKS
  test      Run the test suite with coverage reporting
  deploy    Deploy application to production

Environment Variables

Set environment variables for tasks:

[tool.poe.tasks.test]
cmd = "pytest tests/"
env = {ENVIRONMENT = "test", DEBUG = "true"}

[tool.poe.tasks.serve-prod]
cmd = "uvicorn myapp.main:app"
env = {ENVIRONMENT = "production", DEBUG = "false"}

Load from .env files:

[tool.poe.tasks.serve]
cmd = "uvicorn myapp.main:app"
envfile = ".env"  # Load from .env file

Working Directory Control

Run tasks from specific directories:

[tool.poe.tasks.frontend-build]
cmd = "npm run build"
cwd = "frontend/"  # Run from frontend directory

[tool.poe.tasks.docs]
cmd = "mkdocs serve"
cwd = "docs/"  # Run from docs directory

Task Dependencies

Run prerequisite tasks automatically:

[tool.poe.tasks]
install = "pip install -e .[dev]"
migrate = "python manage.py migrate"

# Run install and migrate before starting server
[tool.poe.tasks.serve]
cmd = "uvicorn myapp.main:app --reload"
deps = ["install", "migrate"]

When you run poe serve, it automatically runs install and migrate first!

Integration with Poetry and uv 🔗

Using Poe with Poetry

Poe automatically detects Poetry projects and uses the Poetry virtual environment:

# Your pyproject.toml (Poetry project)
[tool.poetry.dependencies]
python = "^3.10"
requests = "^2.31.0"

[tool.poetry.group.dev.dependencies]
pytest = "^8.0.0"

[tool.poe.tasks]
test = "pytest tests/"

# Running the task
# Poe automatically uses Poetry's virtualenv!
poe test
# No need for: poetry run pytest tests/

Using Poe with uv

Similar seamless integration with uv:

# Install Poe as dev dependency
uv add --dev poethepoet

# Define tasks in pyproject.toml
[tool.poe.tasks]
test = "pytest tests/"
serve = "uvicorn app.main:app --reload"

# Run tasks
uv run poe test
uv run poe serve

# Or configure Poe to use uv explicitly
[tool.poe]
executor = {type = "uv"}

Standalone (No Poetry/uv)

Poe works without Poetry or uv too:

# Use system Python or standard venv
[tool.poe]
executor = {type = "simple"}

# Or specify virtual environment location
[tool.poe]
executor = {type = "virtualenv", location = ".venv"}

Task Organization Best Practices 📋

Organizing Complex Task Files

For large projects, split tasks into separate file:

# Create poe_tasks.toml in project root
[tool.poe.tasks]
test = "pytest tests/"
serve = "uvicorn app.main:app"
# ... all your tasks here

# In pyproject.toml, just reference it:
[tool.poe]
include = "poe_tasks.toml"

Naming Conventions

✅ Good Task Names:
  • test - Run tests
  • test-unit - Run unit tests specifically
  • test-integration - Run integration tests
  • serve - Start development server
  • serve-prod - Start production server
  • db-migrate - Run database migrations
  • docker-build - Build Docker image
❌ Avoid These:
  • runTests - Use kebab-case, not camelCase
  • t - Too short, not descriptive
  • test_everything_including_integration - Too long
  • do-the-thing - Vague, unclear purpose

Task Categories

Group related tasks with prefixes:

[tool.poe.tasks]
# Testing
test = "pytest tests/"
test-unit = "pytest tests/unit"
test-integration = "pytest tests/integration"
test-cov = "pytest --cov=src"

# Database
db-migrate = "alembic upgrade head"
db-rollback = "alembic downgrade -1"
db-reset = "alembic downgrade base && alembic upgrade head"

# Docker
docker-build = "docker build -t myapp ."
docker-run = "docker run -p 8000:8000 myapp"
docker-clean = "docker system prune -f"

# Documentation
docs-build = "mkdocs build"
docs-serve = "mkdocs serve"
docs-deploy = "mkdocs gh-deploy"

Troubleshooting Common Issues 🔧

Issue 1: "poe: command not found"

Problem: Poe not in PATH after installation

# Solution 1: Ensure pipx path is in PATH
pipx ensurepath
# Then restart terminal

# Solution 2: Use poetry run
poetry run poe --version

# Solution 3: Use uv run
uv run poe --version

# Solution 4: Check installation
pipx list
# Poe should appear in the list

Issue 2: Wrong Virtual Environment

Problem: Poe using wrong Python environment

# Check which environment Poe is using
poe _env

# Specify environment explicitly in pyproject.toml
[tool.poe]
executor = {type = "poetry"}  # Use Poetry's virtualenv

# Or for uv
[tool.poe]
executor = {type = "uv"}

# Or specify virtualenv path
[tool.poe]
executor = {type = "virtualenv", location = ".venv"}

Issue 3: Task Not Running

# Verify task exists
poe --help
# Lists all available tasks

# Check task definition
poe _show test
# Shows the actual command that will run

# Run with verbose mode
poe --verbose test
# Shows detailed execution info

# Check for typos in pyproject.toml
# Make sure [tool.poe.tasks] section exists

Issue 4: Arguments Not Working

# Wrong format - won't accept arguments
[tool.poe.tasks]
test = "pytest ${path}"  # ❌ Missing args definition

# Correct format
[tool.poe.tasks.test]
cmd = "pytest ${path}"
args = [{name = "path", default = "tests/"}]  # ✅

Poe vs Other Task Runners ⚖️

Poe vs Make

Feature Make Poe the Poet
Config File Makefile (separate) pyproject.toml (integrated)
Syntax Tab-sensitive, cryptic TOML, beginner-friendly
Python Integration None (shell only) Native Python support
Virtual Env Handling Manual Automatic
Cross-Platform Limited (needs GNU Make) Full (Python everywhere)

Poe vs npm scripts

Feature npm scripts Poe the Poet
Ecosystem JavaScript/Node.js Python
Task Types Shell commands only Commands, scripts, shell, sequences
Arguments Manual parsing needed Built-in arg handling
Environment Node modules Python virtualenv
✅ Why Choose Poe for Python Projects:
  • Native Python integration (run functions directly)
  • Automatic virtualenv detection (Poetry, uv, venv)
  • Clean TOML syntax (easier than Make)
  • Cross-platform by default
  • Integrated with modern Python tools

Quick Reference Cheat Sheet 📝

# INSTALLATION
pipx install poethepoet                    # Install globally
poetry self add 'poethepoet[poetry-plugin]'  # As Poetry plugin
poetry add --group dev poethepoet          # As project dependency
uv add --dev poethepoet                    # With uv

# RUNNING TASKS
poe task-name                              # Run a task
poe task-name arg1 arg2                    # With arguments
poe task-name --option value               # With options
poe --help                                 # List all tasks
poe _show task-name                        # Show task details
poe _env                                   # Show environment info

# BASIC TASK TYPES
[tool.poe.tasks]
simple = "pytest tests/"                   # Command task
script.script = "module:function()"        # Script task
shell.shell = "echo hello && ls"           # Shell task
sequence.sequence = ["task1", "task2"]     # Sequence task

# TASK WITH ARGUMENTS
[tool.poe.tasks.test]
cmd = "pytest ${path}"
args = [{name = "path", default = "tests/"}]

# TASK WITH OPTIONS
[tool.poe.tasks.serve]
cmd = "uvicorn app:main --port ${port}"
args = [
    {name = "port", options = ["--port", "-p"], default = "8000"}
]

# TASK WITH HELP
[tool.poe.tasks.deploy]
help = "Deploy to production"
cmd = "docker build -t app ."

# TASK WITH ENVIRONMENT
[tool.poe.tasks.test]
cmd = "pytest tests/"
env = {DEBUG = "true", ENV = "test"}
envfile = ".env"

# TASK WITH DEPENDENCIES
[tool.poe.tasks.serve]
cmd = "uvicorn app:main"
deps = ["install", "migrate"]  # Run these first

# EXECUTOR CONFIGURATION
[tool.poe]
executor = {type = "poetry"}               # Use Poetry
executor = {type = "uv"}                   # Use uv
executor = {type = "simple"}               # Simple executor
executor = {type = "virtualenv", location = ".venv"}

Best Practices Checklist ✅

✅ DO These Things:
  • Keep tasks simple: One clear purpose per task
  • Use descriptive names: test, serve, deploy (not t, s, d)
  • Add help text: Especially for complex tasks
  • Group related tasks: test-unit, test-integration
  • Define sequence tasks: For common workflows (ci, pre-commit)
  • Document arguments: Make options clear and discoverable
  • Use task dependencies: Ensure prerequisites run automatically
❌ DON'T Do These:
  • Don't make tasks too long: Break complex tasks into smaller ones
  • Don't use platform-specific commands: Unless necessary (prefer Python)
  • Don't hardcode secrets: Use environment variables or .env files
  • Don't duplicate task logic: Use sequence or dependencies instead
  • Don't forget error handling: Use ignore_fail for sequences when appropriate
💡 Pro Tips:
  • Use shell completion: Run poe _bash_completion or poe _zsh_completion
  • Create a 'check' task: Run all quality checks before committing
  • Use 'dev' task: Start everything needed for development
  • Document in README: List common tasks for new contributors
  • Use verbose mode: Add --verbose when debugging

Summary - Your Poe Journey 🎓

Congratulations! You've learned Poe the Poet from scratch!

What You Now Know:

  • ✅ What Poe the Poet is and why it exists
  • ✅ How to install Poe (multiple methods)
  • ✅ Four types of tasks (command, script, shell, sequence)
  • ✅ How to add arguments and options to tasks
  • ✅ Working with Poetry and uv integration
  • ✅ Real-world project examples
  • ✅ Best practices and common patterns
  • ✅ Troubleshooting common issues

Your Next Steps:

  1. Install Poe using pipx
  2. Add basic tasks to your current project
  3. Create a 'dev' task that starts everything you need
  4. Add a 'ci' sequence task for automated checks
  5. Share your pyproject.toml with your team
  6. Explore advanced features as you need them

Additional Resources 📚

Official Documentation:

  • Poe the Poet Docs: poethepoet.natn.io
  • GitHub Repository: github.com/nat-n/poethepoet
  • PyPI Package: pypi.org/project/poethepoet

Community:

  • GitHub Discussions for questions and ideas
  • GitHub Issues for bug reports
  • Python packaging community resources

Related Tools:

  • Poetry: python-poetry.org
  • uv: github.com/astral-sh/uv
  • pytest, black, ruff (common task tools)

Final Thoughts 💭

Poe the Poet transforms your Python project from a collection of remembered commands into a well-documented, automated workflow that anyone can understand and use.

Whether you're working solo or with a team of 50, Poe ensures everyone runs tasks the same way, every time.

The best part? It's simple to start with basic tasks, and grows with your project as your needs become more complex.

Start small today. Add one task. Then another. Before you know it, your entire workflow is automated, and you'll wonder how you ever lived without Poe! 🎭

Happy task running! ✨

Comments