Bitwise in Python
Introduction
I’ve always had a soft spot for storing configuration using a bitwise approach.
Bitmasks in configuration systems go all the way back to early UNIX systems. It’s actually hard to pinpoint when this pattern truly began — after all, early computers relied on physically toggling bits to define configuration.
Get Wojciech Nowicki’s stories in your inbox
Join Medium for free to get updates from this writer.
In modern high-level languages, this approach is far less common. Knowledge of bitwise operations is no longer something every developer can be assumed to have. That’s why it’s worth starting with a short theoretical refresher.
Theory
Bits
Every byte consists of 8 bits — the smallest units of information, each holding a value of 0 or 1. Bitwise operations allow us to work directly on these bits, which brings several advantages:
- Memory efficiency — a single integer can store 32 or even 64 boolean flags;
- Performance — operations like
AND,OR,XOR, or bit shifts are executed in a single CPU cycle; - Simplicity — instead of managing multiple boolean variables, we keep a consistent set of flags in a single field.
You’ll encounter this approach in Unix file permissions, networking protocols, and computer graphics, among others.
Bitwise operators
| Operator | Name | Example (`a = 0b0101`, `b = 0b0011`) | Result (`bin(...)`)|
| -------- | ----------- | ------------------------------------- | ------------------ |
| `&` | AND | `a & b` | `0b0001` |
| `|` | OR | `a|b` | `0b0111` |
| `^` | XOR | `a ^ b` | `0b0110` |
| `~` | NOT | `~a` (na 32 bitach) | `0b…111010` |
| `<<` | shift left | `a << 1` | `0b1010` |
| `>>` | shift right | `a >> 2` | `0b0001` |Bit flags
- Each flag is assigned a power of two:
1, 2, 4, 8, … - This ensures every flag occupies a unique bit and can coexist independently
- A set of active flags is stored in a single integer
A “raw” example:
READ = 1 # 0b001
WRITE = 2 # 0b010
EXEC = 4 # 0b100
perm = READ | WRITE # enable two flags
can_exec = bool(perm & EXEC) # check EXEC
perm &= ~WRITE # disable WRITEThis works — but it’s easy to make mistakes (magic numbers) and lacks a clean, expressive API.
The BitAware library
BitAware is my attempt to simplify working with bit flags. Instead of dealing with raw integers, it wraps the underlying mechanism in a typed, readable interface:
BitFlag— define a set of flags similarly toenum.IntFlag, but without manually assigning valuesBitAware[T]— a container class that stores a combination of flags of typeTand provides helper methods (has, etc.)- Seamless integration with Pydantic — model fields automatically convert between
intand flag representations
Installation
Using uv:
uv add bitawareOr with pip:
pip install bitawareDeclaration
from bitaware import BitFlag, BitAware
class PermissionTypes(BitFlag):
ADMIN = 1
USER = 2
GUEST = 4
MODERATOR = 8
class UserPermission(BitAware[PermissionTypes]):
def __init__(self, value: int = 0):
super().__init__(value, PermissionTypes)Usage
# Create a user with two permissions
perm = UserPermission(PermissionTypes.ADMIN | PermissionTypes.USER)
perm.has(PermissionTypes.ADMIN) # → True
perm.has(PermissionTypes.GUEST) # → FalseUsing with Pydantic
Now let’s integrate it with Pydantic — and by extension, frameworks like FastAPI.
from pydantic import BaseModel
class User(BaseModel):
name: str
permissions: UserPermission
jane = User(name="Jane", permissions=PermissionTypes.ADMIN | PermissionTypes.USER)
assert jane.permissions.has(PermissionTypes.ADMIN)All standard Pydantic features are supported, including serialization:
print(jane.model_dump_json(indent=2))Which produces:
{
"name": "Jane",
"permissions": 3
}Summary
- Bitwise operations are fast and memory-efficient, but working with raw integers quickly becomes unreadable
- BitAware combines the efficiency of bit flags with the readability and type safety of Python
If your application stores hundreds of thousands of records with simple toggles (feature flags, permissions, statuses), consider using BitAware before building yet another custom solution.
You’ll end up with less code to maintain — and fewer mistakes when dealing with bit-level logic.









