Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 43 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,55 @@ broken link fails the build, and it's faster to find out on your machine.

Forgetting step 2 means your page builds but nobody can find it.

### Adding a blog post

Blog posts are the one exception to step 2. Put the file in `docs/blog/posts/` and the blog index
picks it up, newest first.

```markdown
---
date: 2026-10-10
authors:
- xarlos
description: One sentence.
categories:
- Python releases
---

# Title

Opening paragraph. This is what the blog index shows. Below a `<!-- more -->` tells the blog home where the preview cuts off and you see `more...`

<!-- more -->

The rest of the post.
```

`date` and the `<!-- more -->` line are required, and the build fails without them. Reuse an
existing category before inventing one.

`authors` puts your name and picture on the post. Each entry is a key from `docs/blog/.authors.yml`,
so add yourself there in the same pull request as your first post:

```yaml
authors:
yourkey:
name: Your Name
description: A few words about you
avatar: https://github.com/your-username.png
url: https://github.com/your-username
```

`name` and `avatar` are required. A key that isn't in the file fails the build. A post written by
two people lists both keys.

### Nav conventions

The nav is written out rather than derived from the folder structure. Zensical can derive it, but it
sorts alphabetically, which puts Projects ahead of Rules.

- Every section's `index.md` comes first in its list.
- Everything after that goes in reading order — the page a newcomer needs first goes first, not the
one that's alphabetically luckiest.
- Nest a sub-group only when a section has two clear halves. `projects/` does. Nothing else needs
to.
- Everything after that goes in reading order.

### Front matter

Expand Down
6 changes: 6 additions & 0 deletions docs/blog/.authors.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
authors:
xarlos:
name: Xarlos
description: Zaaar-los. It's like Carlos, but from space.
avatar: https://github.com/Xarlos89.png
url: https://github.com/Xarlos89
7 changes: 7 additions & 0 deletions docs/blog/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
description: Posts about Python and the tech around it, written by the Practical Python community.
---

# Blog

Posts about Python and the tech around it, written by the community. Newest first.
170 changes: 170 additions & 0 deletions docs/blog/posts/python-3-15.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
---
date: 2026-10-10
authors:
- xarlos
description: Python 3.15 is out. Lazy imports, frozendict, unpacking in comprehensions, UTF-8 by default and a new profiler.
categories:
- Python releases
---

# Python 3.15 is out

Python 3.15.0 was released on 9 October 2026. It adds a `lazy` keyword for imports, a built-in
`frozendict`, `*` unpacking inside comprehensions, and a new sampling profiler. It also makes UTF-8
the default text encoding on every platform, which is the change most likely to affect code you have
already written.

<!-- more -->

Everything below comes from the official
[What's new in Python 3.15](https://docs.python.org/3.15/whatsnew/3.15.html), which is the full
list. This is the short one.

## The parts you will notice

### Lazy imports

Put `lazy` in front of an import and the module is not loaded until you first use the name
([PEP 810](https://peps.python.org/pep-0810/)).

```python
lazy import json
lazy from pathlib import Path

print("Starting up...") # json and pathlib are not fully imported yet

data = json.loads('{"key": "value"}') # json gets fully imported here
p = Path(".") # pathlib gets fully imported here
```

This is for programs with slow startup, mostly command-line tools that import a lot and use a little
of it on any one run. The catch: if the module is missing, the error turns up at first use, not on
the import line. A typo in a lazy import can sit there until that code path runs.

### `frozendict`

A dictionary you can't change, and can therefore hash
([PEP 814](https://peps.python.org/pep-0814/)). It is a built-in, so there is nothing to import.

```pycon
>>> a = frozendict(x=1, y=2)
>>> a
frozendict({'x': 1, 'y': 2})
>>> a['z'] = 3
Traceback (most recent call last):
File "<python-input-2>", line 1, in <module>
a['z'] = 3
~^^^^^
TypeError: 'frozendict' object does not support item assignment
>>> b = frozendict(y=2, x=1)
>>> hash(a) == hash(b)
True
>>> a == b
True
```

Being hashable means it can be a dictionary key or a member of a set, which a `dict` can't. It keeps
insertion order but ignores it when comparing, same as `dict`. It is not a subclass of `dict`, so
`isinstance(a, dict)` is `False`.

### Unpacking in comprehensions

`*` and `**` now work inside comprehensions and generator expressions
([PEP 798](https://peps.python.org/pep-0798/)).

```pycon
>>> lists = [[1, 2], [3, 4], [5]]
>>> [*L for L in lists] # equivalent to [x for L in lists for x in L]
[1, 2, 3, 4, 5]

>>> dicts = [{'a': 1}, {'b': 2}, {'a': 3}]
>>> {**d for d in dicts} # equivalent to {k: v for d in dicts for k,v in d.items()}
{'a': 3, 'b': 2}
```

Flattening a list of lists is one of the most-asked questions in the server. The old answer was a
comprehension with two `for` clauses in an order nobody remembers. This one reads the way you would
say it.

### Error messages

`AttributeError` now recognises method names from other languages and tells you the Python one:

```pycon
>>> [1, 2, 3].push(4)
AttributeError: 'list' object has no attribute 'push'. Did you mean '.append'?

>>> 'hello'.toUpperCase()
AttributeError: 'str' object has no attribute 'toUpperCase'. Did you mean '.upper'?

>>> {}.put("a", 1)
AttributeError: 'dict' object has no attribute 'put'. Use d[k] = v.

>>> (1, 2, 3).append(4)
AttributeError: 'tuple' object has no attribute 'append'. Did you mean to use a 'list' object?
```

If you came to Python from JavaScript or Java, this one is for you.

## The part that can break things

Python now uses UTF-8 whenever you don't pass `encoding`
([PEP 686](https://peps.python.org/pep-0686/)). So `open("notes.txt")` reads UTF-8 on every
platform.

On Linux and macOS that was almost always the case already. On Windows the default used to be the
system code page, so a script that reads a file written by an older Windows program may now raise
`UnicodeDecodeError`, or quietly read the wrong characters.

| Situation | What to do |
|----------------------------------------|----------------------------------------------------|
| New code | Pass `encoding="utf-8"` anyway. It works on every version |
| Old file in a legacy encoding | Name it: `open(path, encoding="cp1252")` |
| Need the old behaviour for a whole run | Set `PYTHONUTF8=0` or run with `-X utf8=0` |

## For people who measure things

There is a new `profiling` package ([PEP 799](https://peps.python.org/pep-0799/)).
`profiling.tracing` is the deterministic profiler you know as `cProfile`, which stays as an alias.
`profiling.sampling` is new. It is a sampling profiler called Tachyon that can attach to a process
that is already running, by PID, and write flame graphs. The pure-Python `profile` module is
deprecated and goes away in 3.17.

The experimental JIT compiler got a significant upgrade as well. It is still experimental.

## Smaller things

- A `sentinel` built-in for making unique marker values, the job `object()` has been doing
([PEP 661](https://peps.python.org/pep-0661/)).
- `re.prefixmatch()` is the new name for what `re.match()` does. `re.match()` still works and is
soft deprecated, meaning no removal is planned.
- The `__cached__` module attribute is gone. Use `__spec__.cached`.
- Typing gained `TypedDict` with typed extra items ([PEP 728](https://peps.python.org/pep-0728/))
and `TypeForm` ([PEP 747](https://peps.python.org/pep-0747/)).
- The official macOS installer now includes the free-threaded build by default.

## Should you upgrade?

| You are | Suggestion |
|---------------------------------------|-----------------------------------------------------------------|
| Learning, on your own machine | Yes. The error messages alone are worth it |
| Working on a project with dependencies | Try it in a fresh virtual environment first. Packages with compiled parts can take a few weeks to publish 3.15 builds |
| Running something in production | Wait for 3.15.1 unless you need a feature now |

Python 3.14 is supported until October 2030, so nobody has to hurry.

## Trying it

Installers are on [python.org](https://www.python.org/downloads/). With
[uv](https://docs.astral.sh/uv/), which keeps versions side by side without touching the one you
already have:

```console
$ uv self update
$ uv python install 3.15
$ uv run --python 3.15 python
```

Update uv first. A copy from before the release only knows about the release candidates.

Then type `[1, 2, 3].push(4)` and see what it says.
41 changes: 41 additions & 0 deletions docs/blog/posts/welcome.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
date: 2026-10-10
authors:
- xarlos
description: Why practicalpython.org has a blog now, and what will be posted on it.
categories:
- Site news
---

# The Practical Python blog!

As a community, we have decided to try our hand at a blog.

<!-- more -->

## What goes here

| Kind of post | Example |
|------------------|------------------------------------------------------------------|
| Python releases | For example, Python 5.15 just came out! |
| Tools | A look at something like uv, ruff or a debugger |
| Things we built | How a feature of the bot or this site works, and what went wrong |
| Community news | Events, Code Adventure, changes to how the server runs |

## What doesn't

If you have questions go into Discord, where someone can answer them. Anything that should stay true
for years goes in [Resources](../../resources/index.md) or the
[tutorial](../../python-tutorial/index.md), where people will look for it. A blog post is allowed to
go out of date but we should try to keep resources updated.

## Who writes it

The community, same as the rest of the site. A post is a Markdown file and a pull request. If you
worked something out last week and feel like sharing a win... make a blog post. The
[page about this site](../../projects/our-projects/this-site.md) covers how to build it locally, and
`CONTRIBUTING.md` in the repository has the front matter a post needs.

Not sure whether an idea is worth writing up? Ask in the [Discord]({{ discord_invite }}) first.

The first real post is about [Python 3.15](python-3-15.md).
10 changes: 9 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,19 @@ A Discord community for people who write Python. {{ member_count }} of us so far

[:octicons-arrow-right-24: Read the rules](rules/index.md)

- :material-post-outline:{ .lg .middle } __Blog__

---

Posts about Python and the tech around it, written by the community.

[:octicons-arrow-right-24: Read the blog](blog/index.md)

- :material-bug:{ .lg .middle } __Found a mistake on the site?__

---

Every page here is Markdown in a public repository, written by members. If
Every page here is Markdown in a public repository, written by the community. If
something is wrong, unclear, or missing, open an issue or PR!

[:octicons-arrow-right-24: Contribute!](projects/our-projects/this-site.md)
Expand Down
10 changes: 10 additions & 0 deletions zensical.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ nav = [
{ "Code Adventure" = [
"code-adventure/index.md",
] },
# Posts are not listed here. The blog plugin collects them from
# docs/blog/posts/ and lists them on the index, newest first.
{ "Blog" = [
"blog/index.md",
] },
]

[project.theme]
Expand Down Expand Up @@ -111,6 +116,11 @@ toggle.name = "Switch to light mode"
invalid_links = true
invalid_link_anchors = true

[project.plugins.blog]
blog_dir = "blog"
post_readtime = true
post_excerpt = "required"

[project.markdown_extensions.zensical.extensions.macros]
on_undefined = "strict"
on_error_fail = true
Expand Down
Loading