{"post":{"slug":"how-to-write-a-python-script-2026","title":"How to write a Python script in 2026: structure, tooling, and best practices","excerpt":"Writing a Python script that works once is easy. Writing one that other people can run, debug, and maintain in 2026 requires knowing the right structure, tooling, and modern idioms.","category":"code","date":"March 18, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">W</span>riting a Python script is 10 minutes of work. Writing one that is debuggable six months later, runnable on someone else's machine, and extendable without a full rewrite — that takes knowing the patterns. Python 3.12+ and the 2026 tooling ecosystem make this easier than ever, but only if you start with the right structure.\n\n## What changed in 2026\n\n- **`uv` is the default Python toolchain.** Astral's `uv` replaces pip + venv + pyenv for most use-cases — faster, deterministic, and a single binary. `pip install` still works but new projects start with `uv`.\n- **`pyproject.toml` is universal.** `setup.py`, `setup.cfg`, and bare `requirements.txt` are legacy. Even simple scripts use `pyproject.toml` if they have dependencies.\n- **Python 3.12 pattern matching is widely used.** `match`/`case` is no longer a novelty — expect to read it in colleagues' code.\n- **`ruff` replaced flake8, black, and isort** — one tool, 100x faster, default in nearly every new Python project.\n\n## Script structure template\n\n```\nmy-script/\n├── pyproject.toml\n├── script.py          # or src/mypackage/main.py for larger scripts\n└── README.md\n```\n\n```toml\n# pyproject.toml\n[project]\nname = \"my-script\"\nversion = \"0.1.0\"\nrequires-python = \">=3.12\"\ndependencies = [\n    \"httpx>=0.27\",\n    \"typer>=0.12\",\n]\n\n[tool.ruff]\nline-length = 100\n```\n\n## The minimal well-structured script\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nfetch_data.py — Download and summarize data from a remote API.\n\"\"\"\nfrom __future__ import annotations\n\nimport logging\nimport sys\nfrom pathlib import Path\n\nimport httpx\nimport typer\n\nlogger = logging.getLogger(__name__)\napp = typer.Typer()\n\n\ndef fetch(url: str) -> dict:\n    \"\"\"Fetch JSON from url; raise on non-2xx.\"\"\"\n    response = httpx.get(url, timeout=10)\n    response.raise_for_status()\n    return response.json()\n\n\ndef process(data: dict) -> str:\n    \"\"\"Transform raw data into a summary string.\"\"\"\n    count = len(data.get(\"items\", []))\n    return f\"Found {count} items.\"\n\n\n@app.command()\ndef main(\n    url: str = typer.Argument(..., help=\"API endpoint to fetch\"),\n    output: Path = typer.Option(Path(\"output.txt\"), help=\"Output file\"),\n) -> None:\n    logging.basicConfig(level=logging.INFO, format=\"%(levelname)s %(message)s\")\n    logger.info(\"Fetching %s\", url)\n    data = fetch(url)\n    summary = process(data)\n    output.write_text(summary)\n    logger.info(\"Written to %s\", output)\n\n\nif __name__ == \"__main__\":\n    app()\n```\n\n## Setting up with uv\n\n```bash\n# Install uv (macOS / Linux)\ncurl -LsSf https://astral.sh/uv/install.sh | sh\n\n# Create a new project\nuv init my-script\ncd my-script\n\n# Add dependencies\nuv add httpx typer\n\n# Run the script\nuv run script.py https://api.example.com/data\n\n# Lock dependencies (auto-generated by uv)\n# uv.lock is committed to git\n```\n\n## Argument parsing: argparse vs Typer vs Click\n\n| Library | Best for | 2026 status |\n| --- | --- | --- |\n| `argparse` (stdlib) | Simple scripts, zero deps | Always available |\n| `Typer` | Type-annotated CLIs, fast to write | Most popular in 2026 |\n| `Click` | Complex CLIs, plugins | Mature, large ecosystem |\n| `docopt` | Doc-string-defined interface | Niche use |\n\nFor most scripts: use `argparse` for single-file no-dependency scripts, `Typer` when you are building a real CLI tool.\n\n## Logging vs print\n\n```python\nimport logging\n\n# WRONG for production — no levels, no timestamps, no easy suppression\nprint(\"Starting task...\")\nprint(f\"Error: {e}\")\n\n# CORRECT — structured, configurable, works with log aggregators\nlogging.basicConfig(\n    level=logging.INFO,\n    format=\"%(asctime)s %(levelname)-8s %(name)s %(message)s\",\n)\nlogger = logging.getLogger(__name__)\n\nlogger.info(\"Starting task...\")\nlogger.error(\"Error: %s\", e, exc_info=True)   # includes traceback\n```\n\n## Error handling in scripts\n\n```python\n# Handle specific exceptions; let unexpected ones propagate\ntry:\n    data = fetch(url)\nexcept httpx.HTTPStatusError as e:\n    logger.error(\"HTTP %d from %s\", e.response.status_code, url)\n    sys.exit(1)\nexcept httpx.RequestError as e:\n    logger.error(\"Network error: %s\", e)\n    sys.exit(1)\n```\n\nExit codes matter: 0 for success, non-zero for failure. Cron jobs and CI pipelines depend on them.\n\n## How to pick the right structure\n\n1. **Under 50 lines and single-use?** A flat script with `if __name__ == \"__main__\"` is fine.\n2. **Needs CLI arguments?** Add `argparse` or `Typer` regardless of length.\n3. **Has dependencies?** Add `pyproject.toml` and `uv` from day one.\n4. **Will be run by others or in CI?** Add logging, exit codes, and handle errors explicitly.\n5. **Growing into a real tool?** Move to `src/` layout and add tests with `pytest`.\n\n## Common mistakes\n\n**No `if __name__ == \"__main__\"` guard.** Without it, all module-level code runs on import — your script becomes untestable and dangerous to use as a library.\n\n**Hardcoded paths.** Use `Path(__file__).parent / \"data\"` for paths relative to the script, or CLI arguments for user-supplied paths. Never hardcode `/Users/yourname/...`.\n\n**`except Exception: pass`.** Silent error swallowing. At minimum, log the exception. Usually, re-raise or exit with a non-zero code.\n\n**`print` in production scripts.** Use `logging`. It gives you severity levels, timestamps, and the ability to silence debug output without touching the code.\n\n**No `requirements.txt` or `pyproject.toml`.** Your script works on your machine. Someone else runs it, gets import errors, and gives up. Pin dependencies.\n\n## What to skip\n\n- **`setup.py` and `setuptools` for new scripts** — use `pyproject.toml`; `setup.py` is legacy.\n- **Global `pip install`** — always use a virtual environment (uv handles this automatically).\n- **`os.system()` for shell commands** — use `subprocess.run()` with `check=True` for correct error handling.\n\n## FAQ\n\n**What Python version should I use in 2026?**\nPython 3.12 is the sweet spot — widely supported, fast (15–60% faster than 3.10 on benchmarks), and has all modern features. Python 3.11 is fine too. Avoid 3.9 for new projects.\n\n**Should I use uv or conda?**\n`uv` for most Python scripting and development. `conda` for scientific/ML stacks that need non-Python binary dependencies (CUDA, BLAS, R packages) where conda's environment model helps.\n\n**How do I make my script runnable as a command?**\nAdd a `[project.scripts]` entry in `pyproject.toml`: `my-tool = \"mypackage.main:app\"`. After `uv install`, `my-tool` becomes a command.\n\n**How do I test a script?**\nStructure it so functions are importable, then write `pytest` tests that call those functions directly. The `if __name__ == \"__main__\"` guard makes the module importable without running the CLI.\n\n## Where to go next\n\n- [How to work with APIs in 2026](/blog/how-to-work-with-apis-2026)\n- [How to parse JSON in 2026](/blog/how-to-parse-json-2026)\n- [How to scrape a website in 2026](/blog/how-to-scrape-a-website-2026)","hue":309,"metaTitle":"How to Write a Python Script in 2026: Best Practices","metaDescription":"Step-by-step guide to writing Python scripts in 2026. Covers uv, project structure, argparse, type hints, logging, error handling","keywords":["how to write a python script 2026","python script best practices 2026","python uv tool 2026","python argparse tutorial 2026","python logging guide","python type hints 2026","python scripting tutorial","python cli script 2026","python script structure","modern python scripting 2026"],"tldr":["**Use `uv` to manage your Python version and dependencies** — it is pip + venv + pyenv in one tool and the 2026 default for scripting.","**Structure every non-trivial script with `if __name__ == \"__main__\"`** — makes the script importable as a module and testable.","**Type annotations + `argparse` or Typer** — add CLI arguments and type hints from day one; the cost is low and the debugging payoff is high.","**`logging` over `print`** — print statements vanish in production; structured log output with levels is the correct default.","**Skip:** putting all logic at module level without functions — it makes the script untestable, unreusable, and hard to read."],"readTime":"4 min read","wordCount":971},"related":[{"slug":"how-to-write-a-bash-script-2026","title":"How to write a Bash script in 2026: the safe and practical guide","excerpt":"Bash scripting is still the fastest way to automate repetitive shell work, but most scripts are written unsafely. Here is the 2026-correct approach.","category":"code","date":"April 6, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":306},{"slug":"how-to-scrape-a-website-2026","title":"How to scrape a website in 2026: tools, legality, and best practices","excerpt":"Web scraping in 2026 is harder than it was five years ago — anti-bot systems are sophisticated. But the right tools and ethical approach still get the job done reliably.","category":"code","date":"April 2, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":310},{"slug":"common-python-mistakes-to-avoid-2026","title":"Common Python Mistakes to Avoid in 2026 and How to Fix Them","excerpt":"The Python footguns that still ship bugs in 2026, from mutable default arguments to blocking the async event loop, plus the one-line fix for each.","category":"code","date":"May 5, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":108}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"how-to-write-a-migration-script-2026","title":"How to write a migration script in 2026","excerpt":"Migration scripts move data safely between schemas or systems without downtime. This guide covers patterns, tooling, and rollback strategies for 2026.","category":"code","date":"March 18, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":313},"older":{"slug":"how-to-write-a-recommendation-letter-2026","title":"How to write a recommendation letter in 2026: structure, examples, mistakes","excerpt":"A strong recommendation letter makes a specific case, not a generic one. Here is the exact format, evidence-first approach, and what to skip in 2026.","category":"guides","date":"March 18, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":125}}