Anti-corruption layer: isolating your code from external APIs

Anti-corruption layer: isolating your code from external APIs

Consuming an external API looks harmless at first. You run a requests.get, get a dictionary back, and use it directly throughout the code. The problem starts when that same JSON structure ends up scattered across ten files, and the API renames a field or switches price from float to string. Fixing it becomes a treasure hunt. The anti-corruption layer (ACL) addresses this problem. Borrowed from Domain-Driven Design, it acts as a translator between an external system and your business logic. One contact point, one place to update when the API changes. ...

May 27, 2026 · 4 min · Anthony
Declarative Permissions in DRF with rest_access_policy

Declarative Permissions in DRF with rest_access_policy

Django REST Framework permissions work, but they show their limits as soon as access rules get moderately complex. Multiple roles, objects belonging to a specific user, custom actions on a ViewSet: you end up with has_permission and has_object_permission classes mixing heterogeneous checks, hard to read and even harder to test. rest_access_policy (package djangorestframework-access-policy) takes a different approach: declare access rules as statements, similar to AWS IAM policies. The result is readable at a glance, testable independently of the ViewSet, and extensible without rewriting the entire class. ...

May 26, 2026 · 6 min · Anthony
Hash, HMAC and encryption: securing a Django token

Hash, HMAC and encryption: securing a Django token

A == comparison on a hash is not enough to pick the right mechanism. sha256, HMAC, salted hash, encryption: each approach offers different guarantees. Understanding which ones changes concretely how you store and verify a token in Django. Simple hash import hashlib token_hash = hashlib.sha256(token.encode()).hexdigest() A simple hash is deterministic: the same input always produces the same output. No server secret is involved. It is impossible to recover the original token from the hash (sha256 is a one-way function). But if someone knows or guesses the token, they can recompute the hash and compare. ...

May 25, 2026 · 4 min · Anthony
Python collections : Counter, defaultdict, deque and the rest

Python collections : Counter, defaultdict, deque and the rest

The Python collections module has been part of the standard library since version 2.4. It provides specialized data structures that solve recurring problems without any external dependency. Yet many developers keep writing counting loops, conditional key initialization, or Point classes with x, y, z fields when Counter, defaultdict, and namedtuple do exactly that, better and more readably. Here are the six structures I use regularly, with the cases where they actually make a difference. ...

May 22, 2026 · 7 min · Anthony
Python shutil: copy, move and archive files without subprocess

Python shutil: copy, move and archive files without subprocess

When you need to copy a directory, move files, or create an archive in Python, the temptation is to reach for subprocess.run(["cp", "-r", ...]) or os.system("mv ..."). That approach is fragile, non-portable, and unnecessary: shutil (shell utilities) has been in the Python standard library since version 2.3 and handles all of this cleanly. The Python shutil library is the go-to tool for high-level filesystem operations. Why shutil instead of os or subprocess os exposes low-level system calls: rename, link, create directories. It does not copy file contents. os.rename() fails when the source and destination are on different filesystems (separate partitions, mounted Docker volumes, etc.). subprocess with cp or mv does not work on Windows. ...

May 21, 2026 · 4 min · Anthony
Python operator: itemgetter, attrgetter and the art of replacing lambdas

Python operator: itemgetter, attrgetter and the art of replacing lambdas

The operator library has been part of Python’s standard library forever, and yet many developers keep writing lambda x: x[0] or lambda obj: obj.name when an operator function would do the same job, faster and more readably. Understanding what this library offers, and how it is implemented, changes the way you write functional code in Python. What operator contains operator exposes functions that match the language’s operators. operator.add(2, 3) is the functional equivalent of 2 + 3, operator.lt(a, b) corresponds to a < b. The point is not to replace operators in ordinary arithmetic code, that would be absurd. The point is being able to pass an operation as an argument to a higher-order function (map, filter, sorted, reduce, functools.partial). ...

May 20, 2026 · 7 min · Anthony
Python dataclasses: field(default_factory) in depth

Python dataclasses: field(default_factory) in depth

Python dataclasses automatically generate __init__, __repr__, and __eq__ from type annotations. The moment an attribute needs a mutable default value (list, dict, set), you hit a fundamental Python pitfall. field(default_factory=...) is the solution, and understanding why it is necessary changes how you reason about initialization in Python. The mutable default value trap In Python, default parameter values are evaluated once at function definition time, not at each call. This is a language property, not a bug. ...

May 19, 2026 · 5 min · Anthony
Django: save() skips full_clean() — the validation lifecycle

Django: save() skips full_clean() — the validation lifecycle

Calling obj.save() after defining validators and a clean() method on the model gives the impression that validation is guaranteed. It is not. Django does not call full_clean() during a save(), and this behavior is intentional. Understanding why changes how you architect validation in a project. What save() actually does The lifecycle of a save() call is shorter than you might expect: pre_save signal sent field.pre_save() called on each field (auto_now, auto_now_add, etc.) INSERT or UPDATE SQL based on whether a pk exists post_save signal sent No validation appears anywhere in this sequence. No blank check, no max_length, no call to clean(). The same applies to Model.objects.create() and Model.objects.bulk_create(): all three methods persist without validating. (bulk_create() is covered in depth in Django in_bulk() and bulk_create() if you work with bulk inserts.) ...

May 18, 2026 · 5 min · Anthony
Python __add__ and __iadd__: copy or in-place mutation

Python __add__ and __iadd__: copy or in-place mutation

__add__ and __iadd__ define two distinct behaviors for addition in Python: one creates a new object, the other modifies the existing one. This distinction has real consequences for aliases and shared references, and it catches even experienced developers off guard. add: the addition that creates __add__ is called by the + operator. It must return a new object and leave the operands unchanged. class Vector: def __init__(self, x, y): self.x = x self.y = y def __add__(self, other): return Vector(self.x + other.x, self.y + other.y) a = Vector(1, 2) b = Vector(3, 4) c = a + b # new Vector(4, 6) print(a.x, a.y) # 1 2 — a is unchanged print(id(a) == id(c)) # False — distinct objects a + b calls a.__add__(b). If __add__ is not defined on a or returns NotImplemented, Python tries b.__radd__(a). ...

May 15, 2026 · 4 min · Anthony
Timing attacks in Django with constant_time_compare

Timing attacks in Django with constant_time_compare

A == comparison on a token looks harmless. In practice, it leaks a measurable piece of information: execution time varies depending on how many characters match. That is the principle behind a timing attack, and it is enough for an attacker to reconstruct the token one character at a time. The problem: the comparison that stops too early Python compares strings character by character and stops as soon as a mismatch is found. ...

May 14, 2026 · 3 min · Anthony

Newsletter

Get new articles delivered straight to your inbox.

No spam. Unsubscribe in one click.