Planet Python
Last update: December 21, 2025 07:44 PM UTC
December 21, 2025
Ned Batchelder
Generating data shapes with Hypothesis
In my last blog post (A testing conundrum), I described
trying to test my Hasher class which hashes nested data. I couldn’t get
Hypothesis to generate usable data for my test. I wanted to assert that two
equal data items would hash equally, but Hypothesis was finding pairs like
[0] and [False]. These are equal but hash differently because the
hash takes the types into account.
In the blog post I said,
If I had a schema for the data I would be comparing, I could use it to steer Hypothesis to generate realistic data. But I don’t have that schema...
I don’t want a fixed schema for the data Hasher would accept, but tests to compare data generated from the same schema. It shouldn’t compare a list of ints to a list of bools. Hypothesis is good at generating things randomly. Usually it generates data randomly, but we can also use it to generate schemas randomly!
Hypothesis basics
Before describing my solution, I’ll take a quick detour to describe how Hypothesis works.
Hypothesis calls their randomness machines “strategies”. Here is a strategy that will produce random integers between -99 and 1000:
import hypothesis.strategies as st
st.integers(min_value=-99, max_value=1000)
Strategies can be composed:
st.lists(st.integers(min_value=-99, max_value=1000), max_size=50)
This will produce lists of integers from -99 to 1000. The lists will have up to 50 elements.
Strategies are used in tests with the @given decorator, which takes a
strategy and runs the test a number of times with different example data drawn
from the strategy. In your test you check a desired property that holds true
for any data the strategy can produce.
To demonstrate, here’s a test of sum() that checks that summing a list of numbers in two halves gives the same answer as summing the whole list:
from hypothesis import given, strategies as st
@given(st.lists(st.integers(min_value=-99, max_value=1000), max_size=50))
def test_sum(nums):
# We don't have to test sum(), this is just an example!
mid = len(nums) // 2
assert sum(nums) == sum(nums[:mid]) + sum(nums[mid:])
By default, Hypothesis will run the test 100 times, each with a different randomly generated list of numbers.
Schema strategies
The solution to my data comparison problem is to have Hypothesis generate a random schema in the form of a strategy, then use that strategy to generate two examples. Doing this repeatedly will get us pairs of data that have the same “shape” that will work well for our tests.
This is kind of twisty, so let’s look at it in pieces. We start with a list of strategies that produce primitive values:
primitives = [
st.none(),
st.booleans(),
st.integers(min_value=-1000, max_value=10_000_000),
st.floats(min_value=-100, max_value=100),
st.text(max_size=10),
st.binary(max_size=10),
]
Then a list of strategies that produce hashable values, which are all the primitives, plus tuples of any of the primitives:
def tuples_of(elements):
"""Make a strategy for tuples of some other strategy."""
return st.lists(elements, max_size=3).map(tuple)
# List of strategies that produce hashable data.
hashables = primitives + [tuples_of(s) for s in primitives]
We want to be able to make nested dictionaries with leaves of some other type. This function takes a leaf-making strategy and produces a strategy to make those dictionaries:
def nested_dicts_of(leaves):
"""Make a strategy for recursive dicts with leaves from another strategy."""
return st.recursive(
leaves,
lambda children: st.dictionaries(st.text(max_size=10), children, max_size=3),
max_leaves=10,
)
Finally, here’s our strategy that makes schema strategies:
nested_data_schemas = st.recursive(
st.sampled_from(primitives),
lambda children: st.one_of(
children.map(lambda s: st.lists(s, max_size=5)),
children.map(tuples_of),
st.sampled_from(hashables).map(lambda s: st.sets(s, max_size=10)),
children.map(nested_dicts_of),
),
max_leaves=3,
)
For debugging, it’s helpful to generate an example strategy from this strategy, and then an example from that, many times:
for _ in range(50):
print(repr(nested_data_schemas.example().example()))
Hypothesis is good at making data we’d never think to try ourselves. Here is some of what it made:
[None, None, None, None, None]
{}
[{False}, {False, True}, {False, True}, {False, True}]
{(1.9, 80.64553337755876), (-41.30770818038395, 9.42967906108538, -58.835811641800085), (31.102786990742203,), (28.2724197133397, 6.103515625e-05, -84.35107066147154), (7.436329211943294e-263,), (-17.335739410320514, 1.5029061311609365e-292, -8.17077562035881), (-8.029363284353857e-169, 49.45840191722425, -15.301768150196054), (5.960464477539063e-08, 1.1518373121077722e-213), (), (-0.3262457914511714,)}
[b'+nY2~\xaf\x8d*\xbb\xbf', b'\xe4\xb5\xae\xa2\x1a', b'\xb6\xab\xafEi\xc3C\xab"\xe1', b'\xf0\x07\xdf\xf5\x99', b'2\x06\xd4\xee-\xca\xee\x9f\xe4W']
{'fV': [81.37177374286324, 3.082323424992609e-212, 3.089885728465406e-151, -9.51475773638932e-86, -17.061851038597922], 'J»\x0c\x86肭|\x88\x03\x8aU': [29.549966208819654]}
[{}, -68.48316192397687]
None
['\x85\U0004bf04°', 'pB\x07iQT', 'TRUE', '\x1a5ùZâ\U00048752+¹\U0005fdf8ê', '\U000fe0b9m*¤\U000b9f1e']
(14.232866652585258, -31.193835515904652, 62.29850355163285)
{'': {'': None, 'Ã\U000be8de§\nÈ\U00093608u': None, 'Y\U000709e4¥ùU)GE\U000dddc5¬': None}}
[{(), (b'\xe7', b'')}, {(), (b'l\xc6\x80\xdf\x16\x91', b'', b'\x10,')}, {(b'\xbb\xfb\x1c\xf6\xcd\xff\x93\xe0\xec\xed',), (b'g',), (b'\x8e9I\xcdgs\xaf\xd1\xec\xf7', b'\x94\xe6#', b'?\xc9\xa0\x01~$k'), (b'r', b'\x8f\xba\xe6\xfe\x92n\xc7K\x98\xbb', b'\x92\xaa\xe8\xa6s'), (b'f\x98_\xb3\xd7', b'\xf4+\xf7\xbcU8RV', b'\xda\xb0'), (b'D',), (b'\xab\xe9\xf6\xe9', b'7Zr\xb7\x0bl\xb6\x92\xb8\xad', b'\x8f\xe4]\x8f'), (b'\xcf\xfb\xd4\xce\x12\xe2U\x94mt',), (b'\x9eV\x11', b'\xc5\x88\xde\x8d\xba?\xeb'), ()}, {(b'}', b'\xe9\xd6\x89\x8b')}, {(b'\xcb`', b'\xfd', b'w\x19@\xee'), ()}]
((), (), ())
Finally writing the test
Time to use all of this in a test:
@given(nested_data_schemas.flatmap(lambda s: st.tuples(s, s)))
def test_same_schema(data_pair):
data1, data2 = data_pair
h1, h2 = Hasher(), Hasher()
h1.update(data1)
h2.update(data2)
if data1 == data2:
assert h1.digest() == h2.digest()
else:
# Strictly speaking, unequal data could produce equal hashes,
# but it's very unlikely, so test for it anyway.
assert h1.digest() != h2.digest()
Here I use the .flatmap() method to draw an example from the
nested_data_schemas strategy and call the provided lambda with the drawn
example, which is itself a strategy. The lambda uses st.tuples to make
tuples with two examples drawn from the strategy. So we get one data schema, and
two examples from it as a tuple passed into the test as data_pair. The
test then unpacks the data, hashes them, and makes the appropriate
assertion.
This works great: the tests pass. To check that the test was working well, I made some breaking tweaks to the Hasher class. If Hypothesis is configured to generate enough examples, it finds data examples demonstrating the failures.
I’m pleased with the results. Hypothesis is something I’ve been wanting to use more, so I’m glad I took this chance to learn more about it and get it working for these tests. To be honest, this is way more than I needed to test my Hasher class. But once I got started, I wanted to get it right, and learning is always good.
I’m a bit concerned that the standard setting (100 examples) isn’t enough to find the planted bugs in Hasher. There are many parameters in my strategies that could be tweaked to keep Hypothesis from wandering too broadly, but I don’t know how to decide what to change.
Actually
The code in this post is different than the actual code I ended up with.
Mostly this is because I was working on the code while I was writing this post,
and discovered some problems that I wanted to fix. For example, the
tuples_of function makes homogeneous tuples: varying lengths with
elements all of the same type. This is not the usual use of tuples (see
Lists vs. Tuples). Adapting for heterogeneous tuples added
more complexity, which was interesting to learn, but I didn’t want to go back
and add it here.
You can look at the final strategies.py to see that and other details, including type hints for everything, which was a journey of its own.
Postscript: AI assistance
I would not have been able to come up with all of this by myself. Hypothesis is very powerful, but requires a new way of thinking about things. It’s twisty to have functions returning strategies, and especially strategies producing strategies. The docs don’t have many examples, so it can be hard to get a foothold on the concepts.
Claude helped me by providing initial code, answering questions, debugging when things didn’t work out, and so on. If you are interested, this is one of the discussions I had with it.
December 21, 2025 04:43 PM UTC
December 19, 2025
Luke Plant
Help my website is too small
How can it be a real website if it’s less than 7k?
December 19, 2025 01:45 PM UTC
Real Python
The Real Python Podcast – Episode #277: Moving Towards Spec-Driven Development
What are the advantages of spec-driven development compared to vibe coding with an LLM? Are these recent trends a move toward declarative programming? This week on the show, Marc Brooker, VP and Distinguished Engineer at AWS, joins us to discuss specification-driven development and Kiro.
December 19, 2025 12:00 PM UTC
December 18, 2025
Django Weblog
Hitting the Home Stretch: Help Us Reach the Django Software Foundation's Year-End Goal!
As we wrap up another strong year for the Django community, we wanted to share an update and a thank you. This year, we raised our fundraising goal from $200,000 to $300,000, and we are excited to say we are now over 88% of the way there. That puts us firmly in the home stretch, and a little more support will help us close the gap and reach 100%.
So why the higher goal this year? We expanded the Django Fellows program to include a third Fellow. In August, we welcomed Jacob Tyler Walls as our newest Django Fellow. That extra capacity gives the team more flexibility and resilience, whether someone is taking parental leave, time off around holidays, or stepping away briefly for other reasons. It also makes it easier for Fellows to attend more Django events and stay connected with the community, all while keeping the project running smoothly without putting too much pressure on any one person.
We are also preparing to raise funds for an executive director role early next year. That work is coming soon, but right now, the priority is finishing this year strong.
We want to say a sincere thank you to our existing sponsors and to everyone who has donated so far. Your support directly funds stable Django releases, security work, community programs, and the long-term health of the framework. If you or your organization have end-of-year matching funds or a giving program, this is a great moment to put them to use and help push us past the finish line.
If you would like to help us reach that final stretch, you can find all the details on our fundraising page
Other ways to support Django:
- Benevity Workplace Giving Program: If your employer participates, you can make donations to the DSF via payroll deduction.
- Sponsor Django via GitHub Sponsors: Support Django directly through GitHub's sponsorship platform.
- Official Merch Store: Buy official t-shirts, accessories, and more to support Django.
Thank you for helping support Django and the people who make it possible. We are incredibly grateful for this community and everything you do to keep Django strong.
December 18, 2025 10:04 PM UTC
Sumana Harihareswara - Cogito, Ergo Sumana
Python Software Foundation, National Science Foundation, And Integrity
Python Software Foundation, National Science Foundation, And Integrity
December 18, 2025 07:43 PM UTC
Django Weblog
Introducing the 2026 DSF Board
Thank You to Our Outgoing Directors
We extend our gratitude to Thibaud Colas and Sarah Abderemane, who are completing their terms on the board. Their contributions shaped the foundation in meaningful ways, and the following highlights only scratch the surface of their work.
Thibaud served as President in 2025 and Secretary in 2024. He was instrumental in governance improvements, the Django CNA initiative, election administration, and creating our first annual report. He also led our birthday campaign and helped with the creation of several new working groups this year. His thoughtful leadership helped the board navigate complex decisions.
Sarah served as Vice President in 2025 and contributed significantly to our outreach efforts, working group coordination, and membership management. She also served as a point of contact for the Django CNA initiative alongside Thibaud.
Both Thibaud and Sarah did too many things to list here. They were amazing ambassadors for the DSF, representing the board at many conferences and events. They will be deeply missed, and we are happy to have their continued membership and guidance in our many working groups.
On behalf of the board, thank you both for your commitment to Django and the DSF. The community is better for your service.
Thank You to Our 2025 Officers
Thank you to Tom Carrick and Jacob Kaplan-Moss for their service as officers in 2025.
Tom served as Secretary, keeping our meetings organized and our records in order. Jacob served as Treasurer, providing careful stewardship of the foundation's finances. Their dedication helped guide the DSF through another successful year.
Welcome to Our Newly Elected Directors
We welcome Priya Pahwa and Ryan Cheley to the board, and congratulate Jacob Kaplan-Moss on his re-election.
2026 DSF Board Officers
The board unanimously elected our officers for 2026:
- President: Jeff Triplett
- Vice President: Abigail Gbadago
- Treasurer: Ryan Cheley
- Secretary: Priya Pahwa
- Jacob Kaplan-Moss
- Paolo Melchiorre
- Tom Carrick
I'm honored to serve as President for 2026. The DSF has important work ahead, and I'm looking forward to building on the foundation that previous boards have established.
Our monthly board meeting minutes may be found at dsf-minutes, and December's minutes are available.
If you have a great idea for the upcoming year or feel something needs our attention, please reach out to us via our Contact the DSF page. We're always open to hearing from you.
December 18, 2025 06:50 PM UTC
Ned Batchelder
A testing conundrum
Update: I found a solution which I describe in Generating data shapes with Hypothesis.
In coverage.py, I have a class for computing the fingerprint of a data structure. It’s used to avoid doing duplicate work when re-processing the same data won’t add to the outcome. It’s designed to work for nested data, and to canonicalize things like set ordering. The slightly simplified code looks like this:
class Hasher:
"""Hashes Python data for fingerprinting."""
def __init__(self) -> None:
self.hash = hashlib.new("sha3_256")
def update(self, v: Any) -> None:
"""Add `v` to the hash, recursively if needed."""
self.hash.update(str(type(v)).encode("utf-8"))
match v:
case None:
pass
case str():
self.hash.update(v.encode("utf-8"))
case bytes():
self.hash.update(v)
case int() | float():
self.hash.update(str(v).encode("utf-8"))
case tuple() | list():
for e in v:
self.update(e)
case dict():
for k, kv in sorted(v.items()):
self.update(k)
self.update(kv)
case set():
self.update(sorted(v))
case _:
raise ValueError(f"Can't hash {v = }")
self.hash.update(b".")
def digest(self) -> bytes:
"""Get the full binary digest of the hash."""
return self.hash.digest()
To test this, I had some basic tests like:
def test_string_hashing():
# Same strings hash the same.
# Different strings hash differently.
h1 = Hasher()
h1.update("Hello, world!")
h2 = Hasher()
h2.update("Goodbye!")
h3 = Hasher()
h3.update("Hello, world!")
assert h1.digest() != h2.digest()
assert h1.digest() == h3.digest()
def test_dict_hashing():
# The order of keys doesn't affect the hash.
h1 = Hasher()
h1.update({"a": 17, "b": 23})
h2 = Hasher()
h2.update({"b": 23, "a": 17})
assert h1.digest() == h2.digest()
The last line in the update() method adds a dot to the running hash. That was to solve a problem covered by this test:
def test_dict_collision():
# Nesting matters.
h1 = Hasher()
h1.update({"a": 17, "b": {"c": 1, "d": 2}})
h2 = Hasher()
h2.update({"a": 17, "b": {"c": 1}, "d": 2})
assert h1.digest() != h2.digest()
The most recent change to Hasher was to add the set() clause. There (and in dict()), we are sorting the elements to canonicalize them. The idea is that equal values should hash equally and unequal values should not. Sets and dicts are equal regardless of their iteration order, so we sort them to get the same hash.
I added a test of the set behavior:
def test_set_hashing():
h1 = Hasher()
h1.update({(1, 2), (3, 4), (5, 6)})
h2 = Hasher()
h2.update({(5, 6), (1, 2), (3, 4)})
assert h1.digest() == h2.digest()
h3 = Hasher()
h3.update({(1, 2)})
assert h1.digest() != h3.digest()
But I wondered if there was a better way to test this class. My small one-off tests weren’t addressing the full range of possibilities. I could read the code and feel confident, but wouldn’t a more comprehensive test be better? This is a pure function: inputs map to outputs with no side-effects or other interactions. It should be very testable.
This seemed like a good candidate for property-based testing. The Hypothesis library would let me generate data, and I could check that the desired properties of the hash held true.
It took me a while to get the Hypothesis strategies wired up correctly. I ended up with this, but there might be a simpler way:
from hypothesis import strategies as st
scalar_types = [
st.none(),
st.booleans(),
st.integers(),
st.floats(allow_infinity=False, allow_nan=False),
st.text(),
st.binary(),
]
scalars = st.one_of(*scalar_types)
def tuples_of(strat):
return st.lists(strat, max_size=3).map(tuple)
hashable_types = scalar_types + [tuples_of(s) for s in scalar_types]
# Homogeneous sets: all elements same type.
homogeneous_sets = (
st.sampled_from(hashable_types)
.flatmap(lambda s: st.sets(s, max_size=5))
)
# Full nested Python data.
python_data = st.recursive(
scalars,
lambda children: (
st.lists(children, max_size=5)
| tuples_of(children)
| homogeneous_sets
| st.dictionaries(st.text(), children, max_size=5)
),
max_leaves=10,
)
This doesn’t make completely arbitrary nested Python data: sets are forced to have elements all of the same type or I wouldn’t be able to sort them. Dictionaries only have strings for keys. But this works to generate data similar to the real data we hash. I wrote this simple test:
from hypothesis import given
@given(python_data)
def test_one(data):
# Hashing the same thing twice.
h1 = Hasher()
h1.update(data)
h2 = Hasher()
h2.update(data)
assert h1.digest() == h2.digest()
This didn’t find any failures, but this is the easy test: hashing the same thing twice produces equal hashes. The trickier test is to get two different data structures, and check that their equality matches their hash equality:
@given(python_data, python_data)
def test_two(data1, data2):
h1 = Hasher()
h1.update(data1)
h2 = Hasher()
h2.update(data2)
if data1 == data2:
assert h1.digest() == h2.digest()
else:
assert h1.digest() != h2.digest()
This immediately found problems, but not in my code:
> assert h1.digest() == h2.digest()
E AssertionError: assert b'\x80\x15\xc9\x05...' == b'\x9ap\xebD...'
E
E At index 0 diff: b'\x80' != b'\x9a'
E
E Full diff:
E - (b'\x9ap\xebD...)'
E + (b'\x80\x15\xc9\x05...)'
E Falsifying example: test_two(
E data1=(False, False, False),
E data2=(False, False, 0),
E )
Hypothesis found that (False, False, False) is equal to (False, False, 0),
but they hash differently. This is correct. The Hasher class takes the types of
the values into account in the hash. False and 0 are equal, but they are
different types, so they hash differently. The same problem shows up for
0 == 0.0 and 0.0 == -0.0. The theory of my
test was incorrect: some values that are equal should hash differently.
In my real code, this isn’t an issue. I won’t ever be comparing values like this to each other. If I had a schema for the data I would be comparing, I could use it to steer Hypothesis to generate realistic data. But I don’t have that schema, and I’m not sure I want to maintain that schema. This Hasher is useful as it is, and I’ve been able to reuse it in new ways without having to update a schema.
I could write a smarter equality check for use in the tests, but that would roughly approximate the code in Hasher itself. Duplicating product code in the tests is a good way to write tests that pass but don’t tell you anything useful.
I could exclude bools and floats from the test data, but those are actual values I need to handle correctly.
Hypothesis was useful in that it didn’t find any failures others than the ones I described. I can’t leave those tests in the automated test suite because I don’t want to manually examine the failures, but at least this gave me more confidence that the code is good as it is now.
Testing is a challenge unto itself. This brought it home to me again. It’s not easy to know precisely what you want code to do, and it’s not easy to capture that intent in tests. For now, I’m leaving just the simple tests. If anyone has ideas about how to test Hasher more thoroughly, I’m all ears.
December 18, 2025 10:30 AM UTC
Eli Bendersky
Plugins case study: mdBook preprocessors
mdBook is a tool for easily creating books out of Markdown files. It's very popular in the Rust ecosystem, where it's used (among other things) to publish the official Rust book.
mdBook has a simple yet effective plugin mechanism that can be used to modify the book output in arbitrary …
December 18, 2025 10:10 AM UTC
Peter Bengtsson
Autocomplete using PostgreSQL instead of Elasticsearch
Here on my blog I have a site search. Before you search, there's autocomplete. The autocomplete is solved by using downshift in React and on the backend, there's an API /api/v1/typeahead?q=bla. Up until today, that backend was powered by Elasticsearch. Now it's powered by PostgreSQL. Here's how I implemented it.
Indexing
A cron job loops over all titles in all blog posts and finds portions of the words in the titles as singles, doubles, and triples. For each one, the popularity of the blog post is accumulated to the extracted keywords and combos.
These are then inserted into a Django ORM model that looks like this:
class SearchTerm(models.Model):
term = models.CharField(max_length=100, db_index=True)
popularity = models.FloatField(default=0.0)
add_date = models.DateTimeField(auto_now=True)
index_version = models.IntegerField(default=0)
class Meta:
unique_together = ("term", "index_version")
indexes = [
GinIndex(
name="plog_searchterm_term_gin_idx",
fields=["term"],
opclasses=["gin_trgm_ops"],
),
]
The index_version is used like this, in the indexing code:
current_index_version = (
SearchTerm.objects.aggregate(Max("index_version"))["index_version__max"]
or 0
)
index_version = current_index_version + 1
...
SearchTerm.objects.bulk_create(bulk)
SearchTerm.objects.filter(index_version__lt=index_version).delete()
That means that I don't have to delete previous entries until new ones have been created. So if something goes wrong during the indexing, it doesn't break the API.
Essentially, there are about 13k entries in that model. For a very brief moment there are 2x13k entries and then back to 13k entries when the whole task is done.
Search
The search is done with the LIKE operator.
peterbecom=# select term from plog_searchterm where term like 'za%';
term
-----------------------------
zahid
zappa
zappa biography
zappa biography barry
zappa biography barry miles
zappa blog
(6 rows)
In Python, it's as simple as:
base_qs = SearchTerm.objects.all()
qs = base_qa.filter(term__startswith=term.lower())
But suppose someone searches for bio we want it to match things like frank zappa biography so what it actually does is:
from django.db.models import Q
qs = base_qs.filter(
Q(term__startswith=term.lower()) | Q(term__contains=f" {term.lower()}")
)
Typo tolerance
This is done with the % operator.
peterbecom=# select term from plog_searchterm where term % 'frenk';
term
--------
free
frank
freeze
french
(4 rows)
In the Django ORM it looks like this:
base_qs = SearchTerm.objects.all()
qs = base_qs.filter(term__trigram_similar=term.lower())
And if that doesn't work, it gets even more desperate. It does this using the similarity() function. Looks like this in SQL:
peterbecom=# select term from plog_searchterm where similarity(term, 'zuppa') > 0.14;
term
-------------------
frank zappa
zappa
zappa biography
radio frank zappa
frank zappa blog
zappa blog
zurich
(7 rows)
Note on typo tolerance
Most of the time, the most basic query works and yields results. I.e. the .filter(term__startswith=term.lower()) query.
It's only if it yields fewer results than the pagination size. That's why the fault tolerance query is only-if-needed. This means, it might send 2 SQL select queries from Python to PostgreSQL. In Elasticsearch, you usually don't do this. You send multiple queries and boost the differently.
It can be done with PostgreSQL too using an UNION operator so that you send one but more complex query.
Speed
It's hard to measure the true performance of these things because they're so fast that it's more about the network speed.
On my fast MacBook Pro M4, I ran about 50 realistic queries and measured the time it took each with this new PostgreSQL-based solution versus the previous Elasticsearch solution. They both take about 4ms per query. I suspect, that 90% of that 4ms is serialization & transmission, and not much time inside the database itself.
The number of rows it searches is only, at the time of writing, 13,000+ so it's hard to get a feel for how much faster Elasticsearch would be than PostgreSQL. But with a GIN index in PostgreSQL, it would have to scale much much larger to feel too slow.
About Elasticsearch
Elasticsearch is better than PostgreSQL at full-text search, including n-grams. Elasticsearch is highly optimized for these kinds of things and has powerful ways that you can make a query be a product of how well it matched with each entry's popularity. With PostgreSQL that gets difficult.
But PostgreSQL is simple. It's solid and it doesn't take up nearly as much memory as Elasticsearch.
December 18, 2025 09:46 AM UTC
Talk Python to Me
#531: Talk Python in Production
Have you ever thought about getting your small product into production, but are worried about the cost of the big cloud providers? Or maybe you think your current cloud service is over-architected and costing you too much? Well, in this episode, we interview Michael Kennedy, author of "Talk Python in Production," a new book that guides you through deploying web apps at scale with right-sized engineering.
December 18, 2025 08:00 AM UTC
Seth Michael Larson
Delta emulator adds support for SEGA Genesis games
December 18, 2025 12:00 AM UTC
December 17, 2025
Sebastian Pölsterl
scikit-survival 0.26.0 released
I am pleased to announce that scikit-survival 0.26.0 has been released.
This is a maintainance release that adds support for Python 3.14 and
includes updates to make scikit-survival compatible with new versions
of pandas and osqp.
It adds support for the pandas string dtype,
and copy-on-write, which is going to become the default with pandas 3.
In addition, sksurv.preprocessing.OneHotEncoder
now supports converting columns with the object dtype.
With this release, the minimum supported version are:
| Package | Minimum Version |
|---|---|
| Python | 3.11 |
| pandas | 2.0.0 |
| osqp | 1.0.2 |
Install
scikit-survival is available for Linux, macOS, and Windows and can be installed either
via pip:
pip install scikit-survival
or via conda
conda install -c conda-forge scikit-survival
December 17, 2025 08:26 PM UTC
PyCharm
The Islands theme is now the default look across JetBrains IDEs starting with version 2025.3.This update is more than a visual refresh. It’s our commitment to creating a soft, balanced environment designed to support focus and comfort throughout your workflow. We began introducing the new theme earlier this year, gathering feedback, conducting research, and testing it hands-on with developers […]
December 17, 2025 07:41 PM UTC
Real Python
How to Build the Python Skills That Get You Hired
Build a focused learning plan that helps you identify essential Python skills, assess your strengths, and practice effectively to progress.
December 17, 2025 02:00 PM UTC
Python Morsels
Embrace whitespace
Well placed spaces and line breaks can greatly improve the readability of your Python code.
Table of contents
Whitespace around operators
Compare this:
result = a**2+b**2+c**2
To this:
result = a**2 + b**2 + c**2
I find that second one more readable because the operations we're performing are more obvious (as is the order of operations).
Too much whitespace can hurt readability though:
result = a ** 2 + b ** 2 + c ** 2
This seems like a step backward because we've lost those three groups we had before.
With both typography and visual design, more whitespace isn't always better.
Auto-formatters: both heroes and villains
If you use an auto-formatter …
Read the full article: https://www.pythonmorsels.com/embrace-whitespace/
December 17, 2025 12:00 AM UTC
Armin Ronacher
What Actually Is Claude Code’s Plan Mode?
December 17, 2025 12:00 AM UTC
December 16, 2025
PyCoder’s Weekly
Issue #713: Deprecations, Compression, Functional Programming, and More (Dec. 16, 2025)
December 16, 2025 07:30 PM UTC
Real Python
Exploring Asynchronous Iterators and Iterables
Learn to build async iterators and iterables in Python to handle async operations efficiently and write cleaner, faster code.
December 16, 2025 02:00 PM UTC
Caktus Consulting Group
PydanticAI Agents Intro
In previous posts, we explored function calling and how it enables models to interact with external tools. However, manually defining schemas and managing the request/response loop can get tedious as an application grows. Agent frameworks can help here.
December 16, 2025 01:00 PM UTC
Tryton News
Tryton Release 7.8
We are proud to announce the 7.8 release of Tryton.
This release provides many bug fixes, performance improvements and some fine tuning.
You can give it a try on the demo server, use the docker image or download it here.
As usual upgrading from previous series is fully supported.
Here is a list of the most noticeable changes:
Changes for the User
Client
We added now a drop-down menu to the client containing the user’s notifications. Now when a user clicks on a notification, it is marked as read for this user.
Also we implemented an unread counter in the client and raise a user notification pop-up when a new notification is sent by the server.
Now users can subscribe to a chat of documents by toggling the notification bell-icon.
The chat feature has been activated to many documents like sales, purchases and invoices.
Now we display the buttons that are executed on a selection of records at the bottom of lists.
We now implemented an easier way to search for empty relation fields:
The query Warehouse: = will now return records without a warehouse instead of the former result of records with warehouses having empty names. And the former result can be searched by the following query: "Warehouse.Record Name": =.
Now we interchanged the internal ID by the record name when exporting Many2One and Reference fields to CSV. And the export of One2Many and Many2Many fields is using a list of record names.
We also made it possible to import One2Many field content by using a list of names (like for the Many2Many).
Web
We made the keyboard shortcuts now also working on modals.
Server
On scheduled tasks we now also implemented user notifications.
Each user can now subscribe to be notified by scheduled tasks which generates notifications. Notifications will appear in the client drop-down.
Accounting
On supplier invoice we now made it possible to set a payment reference and to validate it. Per default the Creditor Reference is supported. And on customer invoices Tryton generates a payment reference automatically. It is using the Creditor Reference format by default, and the structured communication for Belgian customers. The payment reference can be validated for defined formats like the “Creditor Reference”. And it can be used in payment rules.
Now we support the Belgian structured communication on invoices, payments and statement rules. And with this the reconciliation process can be automated.
We now implemented when succeeding a group of payments, Tryton now will ask for the clearing date instead of just using today.
Now we store the address of the party in the SEPA mandate instead of using just the first party address.
We now added a button on the accounting category to add or remove multiple products easily.
Customs
Now we support customs agents. They define a party to whom the company is delegating the customs between two countries.
Incoterm
We now added also the old version of Incoterms 2000 because some companies and services are still using it.
Now we allow the modification of the incoterms on the customer shipment as long as it has not yet been shipped.
Product
We now make the list of variants for a product sortable. This is useful for e-commerce if you want to put a specific variant in front.
Now it is possible to set a different list price and gross price per variant without the need for a custom module.
We now made the volume and weight usable in price list formulas. This is useful to include taxes based on such criteria.
Production
Now we made it possible to define phantom bill-of-materials (BOM) to group common inputs or outputs for different BOMs. When used in a production, the phantom BOM is replaced by its corresponding materials.
We now made it possible to define a production as a disassembly. In this case the calculation from the BOM is inverted.
Purchasing
Now we restrict the run of the create purchase wizard from purchase requests which are already purchased.
And also we now restrict to run the create quotation wizard on purchase requests when it is no longer possible to create them.
It is now possible to create a new quotation for a purchase request which already has received one.
Now we made the client to open quotations that have been created by the wizard.
We fine-tuned the supply system: When no supplier can supply on time, the system will now choose the fastest supplier.
Sales
Now we made it possible to encode refunding payments on the sale order.
We allow now to group invoices created for a sale rental with the invoices created for sale orders.
In the sale subscription lines we now implemented a summary column similar to sales.
Stock
We now added two new stock reports that calculates the inventory and turnover of the stock. We find this useful to optimize and fine-tune the order points.
Now we added the support for international shipping to the shipping services: DPD, Sendcloud and UPS.
And now we made Tryton to generate a default shipping description based on the custom categories of the shipped goods (with a fallback to “General Merchandise” for UPS). This is useful for international shipping.
We now implemented an un-split functionality to correct erroneous split moves.
Now we allow to cancel a drop-shipment in state done similar to the other shipment types.
Web Shop
We now define the default Incoterm per web shop to set on the sale orders.
Now we added a status URL to the sales coming from a web shop.
We now added the URL to each product that is published in a web shop.
Now we added a button on sale from the web shop to force an update from the web shop.
We did many improvements to extend our Shopify support:
- Support the credit refunds
- Support of taxes from the shipping product
- Add an option to notify the customers about fulfilment
- Add a set of rules to select the carrier
- Support of product of type “kit”
- Set the “compare-at” price using the non-sale price
- Set the language of the customer to the party
- Add admin URL to each record with a Shopify identifier
New Modules
EDocument Peppol
The EDocument Peppol Module provides the foundation for sending and receiving
electronic documents on the Peppol network.
EDocument Peppol Peppyrus
The EDocument Peppol Peppyrus Module allows sending and receiving electronic
documents on the Peppol network thanks to the free Peppyrus service.
EDocument UBL
The EDocument UBL Module adds electronic documents from UBL.
Sale Rental
The Sale Rental Module manages rental order.
Sale Rental Progress Invoice
The Sale Rental Progress Invoice Module allows creating progress invoices for
rental orders.
Stock Shipment Customs
The Stock Shipment Customs Module enables the generation of commercial
invoices for both customer and supplier return shipments.
Stock Shipping Point
The Stock Shipping Point Module adds a shipping point to shipments.
Changes for the System Administrator
Server
We now made the server stream the JSON and gzip response to reduce the memory consumption.
Now the trytond-console gains an option to execute a script from a file.
We now replaced the [cron] clean_days configuration by [cron] log_size. Now the storage of the logs of scheduled tasks only depends on its size and no longer on its frequency.
Now we made the login process send the URL for the host of the bus. This way the clients do not need to rely on the browser to manage the redirection. Which wasn’t working on recent browsers, anyway.
We now made the login sessions only valid for the IP address of the client that generates it. This enforces the security against session leak.
Now we let the server set a Message-Id header in all sent emails.
Product
We added a timestamp parameter to the URLs of product images. This allows to force a refresh of the old cached images.
Web Shop
Now we added routes to open products, variants, customers and orders using their Shopify-ID. This can be used to customize the admin UI to add a direct link to Tryton.
Changes for the Developer
Server
In this release we introduce notifications. Their messages are sent to the user as soon as they are created via the bus. They can be linked to a set of records or an action that will be opened when the user click on it.
We made it now possible to configure a ModelSQL based on a table_query to be materialized. The configuration defines the interval at which the data must be refreshed and a wizard lets the user force a refresh.
This is useful to optimize some queries for which the data does not need to be exactly fresh but that could benefit from some indexes.
Now we register the models, wizards and reports in the tryton.cfg module file. This reduces the memory consumption of the server. It does no longer need to import all the installed modules but only the activated modules.
This is also a first step to support typing with the Tryton modular design.
We now added the attribute multiple to the <button> on tree view. When set, the button is shown at the bottom of the view.
Now we implemented the declaration of read-only Wizards. Such wizards use a read-only transaction for the execution and because of this write access on the records is not needed.
We now store only immutable structures in the MemoryCache. This prevents the alteration of cached data.
Now we added a new method to the Database to clear the cached properties of the database. This is useful when writing tests that alter those properties.
We now use the SQL FILTER syntax for aggregate functions.
Now we use the SQL EXISTS operator for searching Many2One fields with the where domain operator.
We introduced now the trytond.model.sequence_reorder method to update the sequence field according to the current order of a record list.
Now we refactored the trytond.config to add cache. It is no more needed to retrieve the configuration as a global variable to avoid performance degradation.
We removed the has_window_functions function from the Database, because the feature is supported by all the supported databases.
Now we added to the trytond.tools pair and unpair methods which are equivalent implementation in Python of the sql_pairing.
Proteus
We now implemented the support of total ordering in Proteus Model.
Marketing
We now set the One-Click header on the marketing emails to let the receivers unsubscribe easily.
Sales
Now we renamed the advance payment conditions into lines for more coherence.
Web Shop
We now updated the Shopify module to use the GraphQL API because their REST-API is now deprecated.
4 posts - 2 participants
December 16, 2025 07:00 AM UTC
December 15, 2025
Peter Bengtsson
Comparison of speed between gpt-5, gpt-5-mini, and gpt-5-nano
gpt-5-mini is 3 times faster than gpt-5 and gpt-5-nano.
December 15, 2025 11:37 PM UTC
The Python Coding Stack
If You Love Queuing, Will You Also Love Priority Queuing? • [Club]
Exploring Python’s heapq
December 15, 2025 04:53 PM UTC
Real Python
Writing DataFrame-Agnostic Python Code With Narwhals
If you're a Python library developer looking to write DataFrame-agnostic code, this tutorial will show how the Narwhals library could give you a solution.
December 15, 2025 02:00 PM UTC
Quiz: Writing DataFrame-Agnostic Python Code With Narwhals
If you're a Python library developer wondering how to write DataFrame-agnostic code, the Narwhals library is the solution you're looking for.
December 15, 2025 12:00 PM UTC
Python Bytes
#462 LinkedIn Cringe
Topics include , docs, PyAtlas: interactive map of the top 10,000 Python packages on PyPI., and Buckaroo.







