Skip to content

Commit df72b23

Browse files
authored
Merge pull request #16 from practical-python-org/feature/blog-structure
laying foundations for blogs!
2 parents c0a6bbe + e915d53 commit df72b23

7 files changed

Lines changed: 286 additions & 5 deletions

File tree

‎CONTRIBUTING.md‎

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,55 @@ broken link fails the build, and it's faster to find out on your machine.
1717

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

20+
### Adding a blog post
21+
22+
Blog posts are the one exception to step 2. Put the file in `docs/blog/posts/` and the blog index
23+
picks it up, newest first.
24+
25+
```markdown
26+
---
27+
date: 2026-10-10
28+
authors:
29+
- xarlos
30+
description: One sentence.
31+
categories:
32+
- Python releases
33+
---
34+
35+
# Title
36+
37+
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...`
38+
39+
<!-- more -->
40+
41+
The rest of the post.
42+
```
43+
44+
`date` and the `<!-- more -->` line are required, and the build fails without them. Reuse an
45+
existing category before inventing one.
46+
47+
`authors` puts your name and picture on the post. Each entry is a key from `docs/blog/.authors.yml`,
48+
so add yourself there in the same pull request as your first post:
49+
50+
```yaml
51+
authors:
52+
yourkey:
53+
name: Your Name
54+
description: A few words about you
55+
avatar: https://github.com/your-username.png
56+
url: https://github.com/your-username
57+
```
58+
59+
`name` and `avatar` are required. A key that isn't in the file fails the build. A post written by
60+
two people lists both keys.
61+
2062
### Nav conventions
2163

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

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

3170
### Front matter
3271

‎docs/blog/.authors.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
authors:
2+
xarlos:
3+
name: Xarlos
4+
description: Zaaar-los. It's like Carlos, but from space.
5+
avatar: https://github.com/Xarlos89.png
6+
url: https://github.com/Xarlos89

‎docs/blog/index.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
description: Posts about Python and the tech around it, written by the Practical Python community.
3+
---
4+
5+
# Blog
6+
7+
Posts about Python and the tech around it, written by the community. Newest first.

‎docs/blog/posts/python-3-15.md‎

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
---
2+
date: 2026-10-10
3+
authors:
4+
- xarlos
5+
description: Python 3.15 is out. Lazy imports, frozendict, unpacking in comprehensions, UTF-8 by default and a new profiler.
6+
categories:
7+
- Python releases
8+
---
9+
10+
# Python 3.15 is out
11+
12+
Python 3.15.0 was released on 9 October 2026. It adds a `lazy` keyword for imports, a built-in
13+
`frozendict`, `*` unpacking inside comprehensions, and a new sampling profiler. It also makes UTF-8
14+
the default text encoding on every platform, which is the change most likely to affect code you have
15+
already written.
16+
17+
<!-- more -->
18+
19+
Everything below comes from the official
20+
[What's new in Python 3.15](https://docs.python.org/3.15/whatsnew/3.15.html), which is the full
21+
list. This is the short one.
22+
23+
## The parts you will notice
24+
25+
### Lazy imports
26+
27+
Put `lazy` in front of an import and the module is not loaded until you first use the name
28+
([PEP 810](https://peps.python.org/pep-0810/)).
29+
30+
```python
31+
lazy import json
32+
lazy from pathlib import Path
33+
34+
print("Starting up...") # json and pathlib are not fully imported yet
35+
36+
data = json.loads('{"key": "value"}') # json gets fully imported here
37+
p = Path(".") # pathlib gets fully imported here
38+
```
39+
40+
This is for programs with slow startup, mostly command-line tools that import a lot and use a little
41+
of it on any one run. The catch: if the module is missing, the error turns up at first use, not on
42+
the import line. A typo in a lazy import can sit there until that code path runs.
43+
44+
### `frozendict`
45+
46+
A dictionary you can't change, and can therefore hash
47+
([PEP 814](https://peps.python.org/pep-0814/)). It is a built-in, so there is nothing to import.
48+
49+
```pycon
50+
>>> a = frozendict(x=1, y=2)
51+
>>> a
52+
frozendict({'x': 1, 'y': 2})
53+
>>> a['z'] = 3
54+
Traceback (most recent call last):
55+
File "<python-input-2>", line 1, in <module>
56+
a['z'] = 3
57+
~^^^^^
58+
TypeError: 'frozendict' object does not support item assignment
59+
>>> b = frozendict(y=2, x=1)
60+
>>> hash(a) == hash(b)
61+
True
62+
>>> a == b
63+
True
64+
```
65+
66+
Being hashable means it can be a dictionary key or a member of a set, which a `dict` can't. It keeps
67+
insertion order but ignores it when comparing, same as `dict`. It is not a subclass of `dict`, so
68+
`isinstance(a, dict)` is `False`.
69+
70+
### Unpacking in comprehensions
71+
72+
`*` and `**` now work inside comprehensions and generator expressions
73+
([PEP 798](https://peps.python.org/pep-0798/)).
74+
75+
```pycon
76+
>>> lists = [[1, 2], [3, 4], [5]]
77+
>>> [*L for L in lists] # equivalent to [x for L in lists for x in L]
78+
[1, 2, 3, 4, 5]
79+
80+
>>> dicts = [{'a': 1}, {'b': 2}, {'a': 3}]
81+
>>> {**d for d in dicts} # equivalent to {k: v for d in dicts for k,v in d.items()}
82+
{'a': 3, 'b': 2}
83+
```
84+
85+
Flattening a list of lists is one of the most-asked questions in the server. The old answer was a
86+
comprehension with two `for` clauses in an order nobody remembers. This one reads the way you would
87+
say it.
88+
89+
### Error messages
90+
91+
`AttributeError` now recognises method names from other languages and tells you the Python one:
92+
93+
```pycon
94+
>>> [1, 2, 3].push(4)
95+
AttributeError: 'list' object has no attribute 'push'. Did you mean '.append'?
96+
97+
>>> 'hello'.toUpperCase()
98+
AttributeError: 'str' object has no attribute 'toUpperCase'. Did you mean '.upper'?
99+
100+
>>> {}.put("a", 1)
101+
AttributeError: 'dict' object has no attribute 'put'. Use d[k] = v.
102+
103+
>>> (1, 2, 3).append(4)
104+
AttributeError: 'tuple' object has no attribute 'append'. Did you mean to use a 'list' object?
105+
```
106+
107+
If you came to Python from JavaScript or Java, this one is for you.
108+
109+
## The part that can break things
110+
111+
Python now uses UTF-8 whenever you don't pass `encoding`
112+
([PEP 686](https://peps.python.org/pep-0686/)). So `open("notes.txt")` reads UTF-8 on every
113+
platform.
114+
115+
On Linux and macOS that was almost always the case already. On Windows the default used to be the
116+
system code page, so a script that reads a file written by an older Windows program may now raise
117+
`UnicodeDecodeError`, or quietly read the wrong characters.
118+
119+
| Situation | What to do |
120+
|----------------------------------------|----------------------------------------------------|
121+
| New code | Pass `encoding="utf-8"` anyway. It works on every version |
122+
| Old file in a legacy encoding | Name it: `open(path, encoding="cp1252")` |
123+
| Need the old behaviour for a whole run | Set `PYTHONUTF8=0` or run with `-X utf8=0` |
124+
125+
## For people who measure things
126+
127+
There is a new `profiling` package ([PEP 799](https://peps.python.org/pep-0799/)).
128+
`profiling.tracing` is the deterministic profiler you know as `cProfile`, which stays as an alias.
129+
`profiling.sampling` is new. It is a sampling profiler called Tachyon that can attach to a process
130+
that is already running, by PID, and write flame graphs. The pure-Python `profile` module is
131+
deprecated and goes away in 3.17.
132+
133+
The experimental JIT compiler got a significant upgrade as well. It is still experimental.
134+
135+
## Smaller things
136+
137+
- A `sentinel` built-in for making unique marker values, the job `object()` has been doing
138+
([PEP 661](https://peps.python.org/pep-0661/)).
139+
- `re.prefixmatch()` is the new name for what `re.match()` does. `re.match()` still works and is
140+
soft deprecated, meaning no removal is planned.
141+
- The `__cached__` module attribute is gone. Use `__spec__.cached`.
142+
- Typing gained `TypedDict` with typed extra items ([PEP 728](https://peps.python.org/pep-0728/))
143+
and `TypeForm` ([PEP 747](https://peps.python.org/pep-0747/)).
144+
- The official macOS installer now includes the free-threaded build by default.
145+
146+
## Should you upgrade?
147+
148+
| You are | Suggestion |
149+
|---------------------------------------|-----------------------------------------------------------------|
150+
| Learning, on your own machine | Yes. The error messages alone are worth it |
151+
| 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 |
152+
| Running something in production | Wait for 3.15.1 unless you need a feature now |
153+
154+
Python 3.14 is supported until October 2030, so nobody has to hurry.
155+
156+
## Trying it
157+
158+
Installers are on [python.org](https://www.python.org/downloads/). With
159+
[uv](https://docs.astral.sh/uv/), which keeps versions side by side without touching the one you
160+
already have:
161+
162+
```console
163+
$ uv self update
164+
$ uv python install 3.15
165+
$ uv run --python 3.15 python
166+
```
167+
168+
Update uv first. A copy from before the release only knows about the release candidates.
169+
170+
Then type `[1, 2, 3].push(4)` and see what it says.

‎docs/blog/posts/welcome.md‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
date: 2026-10-10
3+
authors:
4+
- xarlos
5+
description: Why practicalpython.org has a blog now, and what will be posted on it.
6+
categories:
7+
- Site news
8+
---
9+
10+
# The Practical Python blog!
11+
12+
As a community, we have decided to try our hand at a blog.
13+
14+
<!-- more -->
15+
16+
## What goes here
17+
18+
| Kind of post | Example |
19+
|------------------|------------------------------------------------------------------|
20+
| Python releases | For example, Python 5.15 just came out! |
21+
| Tools | A look at something like uv, ruff or a debugger |
22+
| Things we built | How a feature of the bot or this site works, and what went wrong |
23+
| Community news | Events, Code Adventure, changes to how the server runs |
24+
25+
## What doesn't
26+
27+
If you have questions go into Discord, where someone can answer them. Anything that should stay true
28+
for years goes in [Resources](../../resources/index.md) or the
29+
[tutorial](../../python-tutorial/index.md), where people will look for it. A blog post is allowed to
30+
go out of date but we should try to keep resources updated.
31+
32+
## Who writes it
33+
34+
The community, same as the rest of the site. A post is a Markdown file and a pull request. If you
35+
worked something out last week and feel like sharing a win... make a blog post. The
36+
[page about this site](../../projects/our-projects/this-site.md) covers how to build it locally, and
37+
`CONTRIBUTING.md` in the repository has the front matter a post needs.
38+
39+
Not sure whether an idea is worth writing up? Ask in the [Discord]({{ discord_invite }}) first.
40+
41+
The first real post is about [Python 3.15](python-3-15.md).

‎docs/index.md‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,19 @@ A Discord community for people who write Python. {{ member_count }} of us so far
5252

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

55+
- :material-post-outline:{ .lg .middle } __Blog__
56+
57+
---
58+
59+
Posts about Python and the tech around it, written by the community.
60+
61+
[:octicons-arrow-right-24: Read the blog](blog/index.md)
62+
5563
- :material-bug:{ .lg .middle } __Found a mistake on the site?__
5664

5765
---
5866

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

6270
[:octicons-arrow-right-24: Contribute!](projects/our-projects/this-site.md)

‎zensical.toml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ nav = [
5555
{ "Code Adventure" = [
5656
"code-adventure/index.md",
5757
] },
58+
# Posts are not listed here. The blog plugin collects them from
59+
# docs/blog/posts/ and lists them on the index, newest first.
60+
{ "Blog" = [
61+
"blog/index.md",
62+
] },
5863
]
5964

6065
[project.theme]
@@ -111,6 +116,11 @@ toggle.name = "Switch to light mode"
111116
invalid_links = true
112117
invalid_link_anchors = true
113118

119+
[project.plugins.blog]
120+
blog_dir = "blog"
121+
post_readtime = true
122+
post_excerpt = "required"
123+
114124
[project.markdown_extensions.zensical.extensions.macros]
115125
on_undefined = "strict"
116126
on_error_fail = true

0 commit comments

Comments
 (0)