python
52 lines · 7 steps
Copy-on-write attribute snapshots in Python
A wrapper that layers edits over an untouched base object, deferring real mutation until you commit.
Explained by
highlit
1from copy import deepcopy
2
3
4class CowSnapshot:
5 __slots__ = ("_base", "_overrides", "_deleted")
6
7 def __init__(self, base):
8 object.__setattr__(self, "_base", base)
9 object.__setattr__(self, "_overrides", {})
10 object.__setattr__(self, "_deleted", set())
11
12 def __getattr__(self, name):
13 overrides = object.__getattribute__(self, "_overrides")
14 if name in overrides:
15 return overrides[name]
16 if name in object.__getattribute__(self, "_deleted"):
17 raise AttributeError(name)
18 return getattr(object.__getattribute__(self, "_base"), name)
19
20 def __setattr__(self, name, value):
21 object.__getattribute__(self, "_deleted").discard(name)
22 object.__getattribute__(self, "_overrides")[name] = value
23
24 def __delattr__(self, name):
25 overrides = object.__getattribute__(self, "_overrides")
26 overrides.pop(name, None)
27 object.__getattribute__(self, "_deleted").add(name)
28
29 def is_dirty(self):
30 return bool(object.__getattribute__(self, "_overrides")) or bool(
31 object.__getattribute__(self, "_deleted")
32 )
33
34 def commit(self):
35 base = object.__getattribute__(self, "_base")
36 for name, value in object.__getattribute__(self, "_overrides").items():
37 setattr(base, name, value)
38 for name in object.__getattribute__(self, "_deleted"):
39 if hasattr(base, name):
40 delattr(base, name)
41 object.__getattribute__(self, "_overrides").clear()
42 object.__getattribute__(self, "_deleted").clear()
43 return base
44
45 def materialize(self):
46 snapshot = deepcopy(object.__getattribute__(self, "_base"))
47 for name, value in object.__getattribute__(self, "_overrides").items():
48 setattr(snapshot, name, deepcopy(value))
49 for name in object.__getattribute__(self, "_deleted"):
50 if hasattr(snapshot, name):
51 delattr(snapshot, name)
52 return snapshot
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Overriding the attribute dunders lets you intercept every read, write, and delete to build a transparent proxy.
- 2A tombstone set distinguishes 'not set here' from 'explicitly deleted', which a plain overrides dict can't express alone.
- 3Deferring mutation to an explicit commit gives you cheap, discardable edits over shared state.
Related explainers
python
from flask import Blueprint, jsonify from marshmallow import Schema, fields, validate, EXCLUDE from webargs.flaskparser import use_args
Validating query params in Flask with webargs
validation
schema
query-building
Intermediate
10 steps
python
import hashlib import json from fastapi import APIRouter, Request, Response, Depends, HTTPException, status
HTTP ETag caching in a FastAPI route
http-caching
etag
conditional-requests
Intermediate
9 steps
python
from django.db import models from django.db.models import Q from django.conf import settings
Enforcing one default address per user in Django
data modeling
database constraints
partial index
Intermediate
7 steps
python
from typing import Any _MISSING = object()
Recursively diffing two JSON structures
recursion
sentinel
tree-traversal
Intermediate
8 steps
python
from contextlib import contextmanager from typing import Iterator import psycopg2
Streaming Postgres rows with a server-side cursor
generators
context-managers
database-streaming
Intermediate
7 steps
php
<?php namespace App\Http\Requests;
A validated date-range value object in PHP
value-object
validation
immutability
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/copy-on-write-attribute-snapshots-in-python-explained-python-32b3/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.