
My team's requirements.txt last year had 5 separate linting tools configured.
black, flake8, isort, pyflakes, autoflake — all doing overlapping jobs, all needing separate config files, all slowing down CI.
One tool replaced all five. Runs 100x faster. Most people on the team had never heard of it.
That's Python in 2026 — the ecosystem has moved significantly, but most developers are still running 2019-era workflows because no one handed them the upgrade guide.
Here's that guide. No NumPy, no Pandas, no Matplotlib — you know those already.
uv — Replace pip, virtualenv, and pyenv. All three.Written in Rust by the Ruff team. Dependency resolution in milliseconds, not minutes.
uv venv # create env
uv pip install fastapi pydantic # 10-100x faster than pip
uv run --with httpx script.py # inline deps, no pollution
50k+ GitHub stars. Fastest growing Python tool in recent memory.
ruff — One linter to replace fiveSame team as uv. Replaces black + flake8 + isort + pyflakes + autoflake.
ruff check . # lints entire codebase in <1 second
ruff format . # formats like black, faster
One pyproject.toml block. No more juggling five config files.
[tool.ruff]
line-length = 88
select = ["E", "F", "I"]
35k+ stars. If you're not using this, you're maintaining unnecessary complexity.
pydantic v2 — Not just validation anymoreRewritten in Rust. 5–50x faster than v1. But the bigger story: it's now the runtime type system Python never had.
from pydantic import BaseModel, EmailStr, Field
from typing import Annotated
class User(BaseModel):
name: str
email: EmailStr
age: Annotated[int, Field(gt=0, lt=150)]
Backbone of FastAPI, Instructor, PydanticAI, and most serious AI tooling. Not optional in 2026.
polars — DataFrames done rightNot just "faster than Pandas." The API is better. Explicit. No confusing index behavior. Lazy evaluation out of the box.
result = (
pl.scan_csv("big_file.csv") # lazy — nothing loads yet
.filter(pl.col("age") > 25)
.group_by("dept")
.agg(pl.col("salary").mean())
.collect() # executes now, optimized
)
On large datasets, Polars builds a query plan and optimizes before executing — like a SQL engine. 10–100x faster, less memory. 32k+ stars.
httpx — requests with async support that actually worksSame API as requests. Plus HTTP/2. Plus first-class async.
# Sync — familiar
response = httpx.get("https://api.example.com/data")
# Async — runs requests concurrently
async with httpx.AsyncClient() as client:
results = await asyncio.gather(
client.get("/users"),
client.get("/orders"),
client.get("/products"),
)
If your code hits a network and you're not async, you're leaving performance behind.
rich — Terminal output from this decadeprint() debugging, but useful.
from rich.console import Console
console = Console()
console.print_json('{"name": "Alice"}') # syntax highlighted
console.print(table) # beautiful tables
for item in track(items, "Processing..."): # progress bar
process(item)
50k+ stars. Also replaces the default Python traceback with one that's readable.
loguru — Logging without the 20-line setupPython's built-in logging works. It also requires a lot of boilerplate before it does anything useful. Loguru works at import.
from loguru import logger
logger.info("Started")
logger.add("logs/app_{time}.log", rotation="500 MB", retention="10 days")
logger.bind(user_id=123).info("Request processed")
Colored output, file rotation, structured context — all zero config.
typer — CLIs that document themselvesargparse is filling out paperwork. Typer is writing Python.
@app.command()
def process(
input: str = typer.Argument(..., help="Input file path"),
verbose: bool = typer.Option(False, "-v"),
limit: int = typer.Option(100),
):
"""Process input file and output results."""
...
Auto-generates --help. Validates types. Supports subcommands. Built on Click without the ceremony.
duckdb — SQL inside your Python script, no server neededSQLite is for transactional workloads. DuckDB is for analytics — aggregations, joins, column operations on large datasets.
# Query CSV directly. No schema. No import.
result = duckdb.sql("""
SELECT dept, AVG(salary), COUNT(*)
FROM 'employees.csv'
WHERE hire_date > '2022-01-01'
GROUP BY dept
""").fetchdf() # returns a DataFrame
Columnar storage. Millions of rows in seconds. No server. No setup. 26k+ stars.
marimo — Jupyter but reproducibleTwo things wrong with Jupyter: cells run out of order (hidden state), and git diffs are unreadable JSON.
marimo fixes both. Notebooks are stored as plain .py files. Cells execute based on data dependencies, not order.
# Reactive — change this slider, dependent cells auto-rerun
data = mo.ui.slider(1, 100, label="Sample Size")
chart = mo.ui.table(generate_data(data.value))
18k stars in ~1 year. The data community is watching.
instructor — Structured output from LLMs, reliablyAsk an LLM for JSON. Get mostly JSON. Sometimes broken. Sometimes missing fields.
Instructor wraps any LLM client and uses Pydantic models to define exactly what you want back. Auto-retries with validation errors as context if the model gets it wrong.
class Person(BaseModel):
name: str
age: int
skills: list[str]
person = client.chat.completions.create(
model="gpt-4o",
response_model=Person,
messages=[{"role": "user", "content": "John is a 32-year-old Python dev..."}]
)
# person.skills → ["Python", "FastAPI", ...] — always typed, always valid
12k stars. Essential for any production LLM pipeline.
pydantic-ai — Agent framework that looks like Python, not magicMost agent frameworks are overengineered. PydanticAI follows the same philosophy as Pydantic: type safety, explicit interfaces, no magic.
agent = Agent(
"openai:gpt-4o",
result_type=WeatherResult, # typed output
system_prompt="You are a weather assistant."
)
result = await agent.run("Weather in Tokyo?")
print(result.data.recommendation) # typed, not raw text
Dependency injection for testing. Multi-model support. Streaming. The right abstraction level. 14k stars.
markitdown — Any document → Markdown in one lineMicrosoft released this quietly in late 2024.
md = MarkItDown()
result = md.convert("report.pdf") # PDF
result = md.convert("data.xlsx") # Excel
result = md.convert("deck.pptx") # PowerPoint
result = md.convert("https://...") # URL
Converts PDF, Word, Excel, PowerPoint, HTML, images (OCR), audio → clean Markdown. The obvious use case is feeding documents to LLMs.
86,000 GitHub stars. Not a typo.
tenacity — Retry logic without the while loopNetwork calls fail. APIs rate-limit. Databases have transient errors. The naive implementation is always a messy while loop.
@retry(
stop=stop_after_attempt(5),
wait=wait_exponential(min=1, max=60),
retry=retry_if_exception_type(httpx.TimeoutException)
)
async def fetch(url: str):
...
Composable, configurable, async + sync. Handles exponential backoff, jitter, and logging automatically.
structlog — Logs as data, not stringsLoguru for dev. structlog for production.
log.info("payment_failed",
user_id=123,
error_code="insufficient_funds",
amount=99.99
)
Dev output: readable key-value pairs. Production output: clean JSON that Datadog/Elasticsearch can parse and index.
SELECT * FROM logs WHERE error_code = 'insufficient_funds' — that's what structured logs give you.
anyio — Async code that runs on any backendasyncio and trio. Code written for one doesn't work on the other. anyio abstracts both.
async with anyio.create_task_group() as tg:
for item in items:
tg.start_soon(process_single, item)
anyio.run(main) # runs on asyncio
anyio.run(main, backend="trio") # same code, runs on trio
FastAPI, Starlette, and httpx are all built on anyio. You're already using it indirectly.
fastapi deeper — You're using ~30% of itYou probably know FastAPI. Here's what most people skip:
@app.post("/orders", response_model=OrderResponse, status_code=201)
async def create_order(
order: OrderCreate, # validated automatically
token: Annotated[str, Depends(security)], # injected automatically
db: Annotated[Session, Depends(get_db)] # DI — easy to mock in tests
) -> OrderResponse:
...
Background tasks, WebSockets, middleware, dependency injection, OpenAPI docs — all from type annotations. State of Python 2025 survey: FastAPI now leads Flask in new project adoption.
stamina — Production retries with zero configTenacity when you need control. stamina when you just want it to work correctly.
@stamina.retry(on=httpx.HTTPError, attempts=5)
def fetch_user(user_id: int):
response = httpx.get(f"/users/{user_id}")
response.raise_for_status()
return response.json()
Adds jitter by default (prevents thundering herd). Integrates with structlog. Toggle off in tests with one line: stamina.set_active(False).
Rust is eating Python's tooling layer. uv, Ruff, Polars, Pydantic v2 — all have Rust cores. Python stays as the interface; Rust handles the performance-critical parts.
Type hints are no longer optional. Pydantic, FastAPI, Instructor, PydanticAI — all built around annotations. Writing untyped Python in 2026 is actively making your life harder.
The LLM tooling layer is finally maturing. First wave: demos. Instructor and PydanticAI are the second wave — production-grade tools with validation, error handling, typed interfaces.
Where to start if you're not already:
Still using pip for everything → switch to uv today
Messy linting setup → ruff this week
Building with LLMs → instructor immediately
Data work → give polars one real project
The gap between developers who've adopted this stack and those who haven't is growing. The work takes the same amount of time; the experience is just completely different.
What am I missing from your stack? Drop it in the comments.
0
10
1