From a2bc2263d48f8a3c08f545f3966c015268c5f732 Mon Sep 17 00:00:00 2001 From: Xarlos <57622136+Xarlos89@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:41:01 +0200 Subject: [PATCH 1/5] the base for the Practical Python blog! --- CONTRIBUTING.md | 30 +++++- docs/blog/index.md | 7 ++ docs/blog/posts/python-3-15.md | 168 +++++++++++++++++++++++++++++++++ docs/blog/posts/welcome.md | 38 ++++++++ docs/index.md | 10 +- zensical.toml | 10 ++ 6 files changed, 258 insertions(+), 5 deletions(-) create mode 100644 docs/blog/index.md create mode 100644 docs/blog/posts/python-3-15.md create mode 100644 docs/blog/posts/welcome.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a013462..1fa9377 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,16 +17,38 @@ 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 +description: One sentence. +categories: + - Python releases +--- + +# Title + +Opening paragraph. This is what the blog index shows. Below a `` tells the blog home where the preview cuts off and you see `more...` + + + +The rest of the post. +``` + +`date` and the `` line are required, and the build fails without them. Reuse an +existing category before inventing one. + ### 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 diff --git a/docs/blog/index.md b/docs/blog/index.md new file mode 100644 index 0000000..e6de338 --- /dev/null +++ b/docs/blog/index.md @@ -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. diff --git a/docs/blog/posts/python-3-15.md b/docs/blog/posts/python-3-15.md new file mode 100644 index 0000000..d721e4e --- /dev/null +++ b/docs/blog/posts/python-3-15.md @@ -0,0 +1,168 @@ +--- +date: 2026-10-10 +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. + + + +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 "", line 1, in + 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. diff --git a/docs/blog/posts/welcome.md b/docs/blog/posts/welcome.md new file mode 100644 index 0000000..9aa6e64 --- /dev/null +++ b/docs/blog/posts/welcome.md @@ -0,0 +1,38 @@ +--- +date: 2026-10-10 +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. + + + +## 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). diff --git a/docs/index.md b/docs/index.md index ad8c630..bf03c53 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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) diff --git a/zensical.toml b/zensical.toml index 21a25e3..2f763e9 100644 --- a/zensical.toml +++ b/zensical.toml @@ -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] @@ -110,6 +115,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 From d1b607b86116e11deb2ab60b8faded8146e47edc Mon Sep 17 00:00:00 2001 From: Xarlos <57622136+Xarlos89@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:41:28 +0200 Subject: [PATCH 2/5] pre-commit --- docs/blog/posts/welcome.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/blog/posts/welcome.md b/docs/blog/posts/welcome.md index 9aa6e64..155c655 100644 --- a/docs/blog/posts/welcome.md +++ b/docs/blog/posts/welcome.md @@ -22,15 +22,16 @@ As a community, we have decided to try our hand at a blog. ## 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. +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 +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. From 16f86ac464c221951ddb594500133e22b412fde8 Mon Sep 17 00:00:00 2001 From: Xarlos <57622136+Xarlos89@users.noreply.github.com> Date: Sat, 10 Oct 2026 17:01:02 +0200 Subject: [PATCH 3/5] Add blog authors with profile pics from GitHub! --- CONTRIBUTING.md | 17 +++++++++++++++++ docs/blog/.authors.yml | 6 ++++++ docs/blog/posts/python-3-15.md | 2 ++ docs/blog/posts/welcome.md | 2 ++ 4 files changed, 27 insertions(+) create mode 100644 docs/blog/.authors.yml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1fa9377..5d27d57 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,6 +25,8 @@ picks it up, newest first. ```markdown --- date: 2026-10-10 +authors: + - xarlos description: One sentence. categories: - Python releases @@ -42,6 +44,21 @@ The rest of the post. `date` and the `` 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 diff --git a/docs/blog/.authors.yml b/docs/blog/.authors.yml new file mode 100644 index 0000000..d05e2c0 --- /dev/null +++ b/docs/blog/.authors.yml @@ -0,0 +1,6 @@ +authors: + xarlos: + name: Xarlos + description: Practical Python + avatar: https://github.com/Xarlos89.png + url: https://github.com/Xarlos89 diff --git a/docs/blog/posts/python-3-15.md b/docs/blog/posts/python-3-15.md index d721e4e..581ebad 100644 --- a/docs/blog/posts/python-3-15.md +++ b/docs/blog/posts/python-3-15.md @@ -1,5 +1,7 @@ --- 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 diff --git a/docs/blog/posts/welcome.md b/docs/blog/posts/welcome.md index 155c655..3cf2e79 100644 --- a/docs/blog/posts/welcome.md +++ b/docs/blog/posts/welcome.md @@ -1,5 +1,7 @@ --- date: 2026-10-10 +authors: + - xarlos description: Why practicalpython.org has a blog now, and what will be posted on it. categories: - Site news From 5210922a287d38eb86bc7f8b2a80cfad77e1e268 Mon Sep 17 00:00:00 2001 From: Xarlos <57622136+Xarlos89@users.noreply.github.com> Date: Sat, 10 Oct 2026 17:01:23 +0200 Subject: [PATCH 4/5] pre-commit --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5d27d57..66264fa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,8 +44,8 @@ The rest of the post. `date` and the `` 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: +`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: From 87d7f15de854cae1692d793a5e5a1a919befdedb Mon Sep 17 00:00:00 2001 From: Xarlos <57622136+Xarlos89@users.noreply.github.com> Date: Sat, 10 Oct 2026 17:03:45 +0200 Subject: [PATCH 5/5] update my description --- docs/blog/.authors.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/blog/.authors.yml b/docs/blog/.authors.yml index d05e2c0..a02e960 100644 --- a/docs/blog/.authors.yml +++ b/docs/blog/.authors.yml @@ -1,6 +1,6 @@ authors: xarlos: name: Xarlos - description: Practical Python + description: Zaaar-los. It's like Carlos, but from space. avatar: https://github.com/Xarlos89.png url: https://github.com/Xarlos89