Good Python reads almost like plain English. Most of that comes from a handful of language features and a few tools used well. This guide collects the ones worth using every day: modern idioms, virtual environments, testing with pytest and Django, and running Python in the browser with PyScript.
| Tool | Current line on 30 Oct 2026 | Source |
|---|---|---|
| Python | 3.15 (released 9 Oct 2026) and 3.14 | devguide.python.org |
| Django LTS | 5.2, supported until April 2028 | djangoproject.com |
| Django latest | 6.1 | djangoproject.com |
| PyScript | 2026.7.3 | pyscript.net |
Idioms worth using
f-strings
Put values straight into a string, with formatting after a colon. Adding = prints the expression and its value, which is handy for quick debugging.
name, total = "Eleni", 1234.5
print(f"{name} owes {total:,.2f} EUR") # Eleni owes 1,234.50 EUR
print(f"{total=}") # total=1234.5
Comprehensions
A comprehension builds a list, dict or set in one readable line. Use a generator expression (round brackets) when you only need to loop over the result once, such as inside sum(). If a comprehension needs two conditions and a nested loop, write a normal loop instead.
prices = [12, 40, 7, 99]
cheap = [p for p in prices if p < 20] # [12, 7]
by_id = {u.id: u for u in users} # dict comprehension
total = sum(p * 1.19 for p in prices) # generator, no list built
Dataclasses
A dataclass writes __init__, __repr__ and comparison methods for you. Use field(default_factory=list) for mutable defaults, never items=[], which would share one list between every object. slots=True saves memory and catches typos in attribute names.
from dataclasses import dataclass, field
@dataclass(slots=True)
class Order:
id: int
customer: str
items: list[str] = field(default_factory=list)
paid: bool = False
o = Order(1, "Andreas")
print(o) # Order(id=1, customer='Andreas', items=[], paid=False)
pathlib
pathlib treats paths as objects. The / operator joins them on every operating system, and methods such as read_text() and glob() replace a lot of os.path code.
from pathlib import Path
reports = Path("data") / "reports"
reports.mkdir(parents=True, exist_ok=True)
for f in reports.glob("*.csv"):
print(f.name, f.stat().st_size)
text = (reports / "notes.txt").read_text(encoding="utf-8")
enumerate and zip
Skip manual counters and index juggling. enumerate gives you the position, zip walks several lists together. Pass strict=True so a length mismatch fails loudly instead of silently dropping items.
for line_no, line in enumerate(lines, start=1):
print(line_no, line)
for name, score in zip(names, scores, strict=True):
print(name, score) # strict=True raises if the lengths differ
match
Structural pattern matching, added in Python 3.10, checks the shape of data and pulls values out of it in one step. It shines on parsed JSON and messages with a type field. For a simple comparison against two or three values, an if chain is still clearer.
def handle(event: dict) -> str:
match event:
case {"type": "payment", "amount": amount} if amount > 0:
return f"Paid {amount}"
case {"type": "refund"}:
return "Refund"
case _:
return "Unknown event"
Virtual environments, pip and uv
Give every project its own virtual environment so its packages never clash with another project's or with the system Python. The standard library's venv plus pip works everywhere. uv is a newer, very fast tool that creates the environment, installs packages, records exact versions in uv.lock and can install Python itself.
# Standard library only
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install requests
pip freeze > requirements.txt
# With uv
uv init shop && cd shop
uv add requests
uv add --dev pytest
uv run python main.py
Either way, commit the dependency list (requirements.txt, or pyproject.toml and uv.lock) and keep .venv out of version control.
Testing with pytest
pytest finds files named test_*.py and runs every function whose name starts with test_. A plain assert is enough; when it fails, pytest shows both sides of the comparison. parametrize runs one test with many inputs.
# test_prices.py
import pytest
from prices import with_vat
def test_with_vat_adds_19_percent():
assert with_vat(100) == 119
@pytest.mark.parametrize("bad", [-1, None, "ten"])
def test_with_vat_rejects_bad_input(bad):
with pytest.raises(ValueError):
with_vat(bad)
Run pytest (or uv run pytest) from the project folder. Add -x to stop at the first failure and -k vat to run only tests whose names contain "vat".
Testing a Django app
For new Django projects, use the long-term support release, Django 5.2, unless you need a feature from 6.x. Django's own test runner creates a separate test database, and each TestCase test runs inside a transaction that is rolled back afterwards, so tests do not leak data into each other. setUpTestData creates shared rows once per class, which is much faster than setUp for every test. The built-in self.client makes requests without a running server.
# shop/tests.py
from django.test import TestCase
from django.urls import reverse
from .models import Product
class ProductPageTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.product = Product.objects.create(name="Mug", price=12)
def test_detail_page_shows_name(self):
url = reverse("product-detail", args=[self.product.pk])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Mug")
python manage.py test # every test
python manage.py test shop # one app
python manage.py test --parallel # use all CPU cores
The Django testing documentation covers the test client, fixtures and browser tests. If you prefer pytest's style, the pytest-django plugin runs Django tests with pytest.
PyScript: Python in the browser
PyScript runs Python inside a web page using WebAssembly. It is good for teaching, data demos and small tools that run without a server. Early tutorials loaded it from an "alpha" URL that no longer exists. Today you load a dated release from pyscript.net/releases/ and write code in a <script type="py"> tag:
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="https://pyscript.net/releases/2026.7.3/core.css">
<script type="module" src="https://pyscript.net/releases/2026.7.3/core.js"></script>
</head>
<body>
<button id="go">Say hello</button>
<p id="out"></p>
<script type="py">
from pyscript import display, when
@when("click", "#go")
def hello(event):
display("Hello from Python in the browser", target="out")
</script>
</body>
</html>
Pin the release in the URL, as above, so an update never changes your page without warning. The first load downloads the Python runtime, which takes a few seconds, so PyScript suits pages where that wait is acceptable. The PyScript documentation explains how to add packages and how to call JavaScript from Python.
Common questions
- Should I switch from pip to uv?
- If pip and venv work for you, there is no rush. uv is worth trying on a new project: it is much faster and the lock file makes every install identical.
- Which Python version should I use for a new project?
- Python 3.14 is a safe choice today. Python 3.15 is newer; move to it once the libraries you depend on support it.
- pytest or the Django test runner?
- Both work well. The built-in runner needs nothing extra. pytest with pytest-django gives you plain asserts, fixtures and parametrize.
- Is PyScript ready for production?
- It is fine for demos, teaching and internal tools. For a public site where load time matters, measure the first load before you commit to it.
New to programming? Which programming language should you learn first? helps you choose. When your Django app is ready, Deploying a React or Django app takes it to a server, and Getting paid on Internetivo explains how professionals find paid tasks.