Type-safe property path navigation for Python
Project description
keyof
Type-safe property paths for Python, inspired by TypeScript's keyof.
What it does
from dataclasses import dataclass
from keyof import KeyOf
@dataclass
class User:
name: str
email: str
# Define a path, your IDE autocompletes, your type checker validates
path = KeyOf(lambda u: u.name)
# Extract the value
user = User(name="Alice", email="alice@example.com")
path.from_(user) # → "Alice"
# Serialize for APIs, configs, logs...
str(path) # → "name"
Installation
pip install py-keyof
Why? 🤔
String-based paths like "user.address.city" are convenient, but they break silently when you rename a field. Your IDE can't autocomplete them, and typos only surface at runtime (usually in production, at 3am).
keyof uses a lambda to capture the path. Pylance/Pyright sees the lambda and validates every attribute access, so you get squiggly red lines in your editor instead of surprises in production.
How it works (and Python's limitations)
Python doesn't have TypeScript's keyof operator, so we get a bit creative:
- At runtime, the lambda receives a proxy object that records attribute accesses
- At type-check time, the lambda is analyzed normally by Pylance/Pyright
The path gets validated statically, but the mechanism is admittedly a bit of a hack. The lambda never actually runs on real data, it only runs once during KeyOf construction to capture the path.
Handling optional fields 🔗
When a field is T | None, accessing attributes on it is a type error. Use nn() to tell the type checker "trust me, this isn't None":
from keyof import KeyOf, nn
@dataclass
class User:
address: Address | None
# nn() strips None from the type (no-op at runtime)
path = KeyOf(lambda u: nn(u.address).city)
# Or with pipe syntax, if that's more your style
path = KeyOf(lambda u: (u.address | nn).city)
This is similar to TypeScript's ! or Kotlin's !!. The path will still fail at runtime if the value is actually None. Use path.from_(obj, default=...) to handle that gracefully.
Working with dicts and lists
Bracket access works too:
data = {"users": [{"name": "Alice"}]}
path = KeyOf(lambda d: d["users"][0]["name"])
path.from_(data) # → "Alice"
Path introspection 🔍
path = KeyOf(lambda u: u.address.city)
path.parts # → ("address", "city")
path.depth # → 2
path.root # → "address"
path.leaf # → "city"
path.parent() # → KeyOf for "address"
Serialization
Need your path in a different format? keyof got you covered:
path = KeyOf(lambda u: u.address.city)
str(path) # → "address.city"
path.to_jsonpath() # → "$.address.city"
path.to_bracket() # → "['address']['city']"
path.to_posix() # → "address/city"
path.to_posix(from_root=True) # → "/address/city"
path.to_xpath() # → "address/city"
path.to_xpath(from_root=True) # → "/address/city"
Equality and hashing
Paths are immutable and can be compared, hashed, and used in sets/dicts:
p1 = KeyOf(lambda u: u.name)
p2 = KeyOf(lambda u: u.name)
p1 == p2 # True
p1 == "name" # True (compares string representation)
{p1, p2} # Set with one element
Limitations ⚠️
- The lambda trick only works for attribute and item access. Method calls, arithmetic, or other expressions won't work.
- Pipe syntax
(x | nn)needs parentheses because.binds tighter than|. - There's no way to express "this path might not exist" at the type level. That's a runtime concern, handled by the
defaultparameter.
AI disclaimer 🤖
Some of the files in this project were generated with the help of AI, especially the config files and project scaffolding. Github Copilot suggestions were used for some of the implementation, tests, and documentation. While I strive to ensure its correctness and quality, please be aware that it may contain errors or suboptimal patterns. Always review and test the code before using it in production.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file py_keyof-1.1.0.tar.gz.
File metadata
- Download URL: py_keyof-1.1.0.tar.gz
- Upload date:
- Size: 12.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
664783a538df7275a8c41bf8dade558957c057a1660980220d88f3660acd517c
|
|
| MD5 |
3813873941156a19a66962186215cc4f
|
|
| BLAKE2b-256 |
b623d8df12249fec07809b7175fba5f4ca0d79285f57f457788dbe26df455502
|
Provenance
The following attestation bundles were made for py_keyof-1.1.0.tar.gz:
Publisher:
ci.yml on eyusd/keyof
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
py_keyof-1.1.0.tar.gz -
Subject digest:
664783a538df7275a8c41bf8dade558957c057a1660980220d88f3660acd517c - Sigstore transparency entry: 1567259185
- Sigstore integration time:
-
Permalink:
eyusd/keyof@1786850c1edaf74905b722fbcb5f7143f5e48475 -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/eyusd
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@1786850c1edaf74905b722fbcb5f7143f5e48475 -
Trigger Event:
release
-
Statement type:
File details
Details for the file py_keyof-1.1.0-py3-none-any.whl.
File metadata
- Download URL: py_keyof-1.1.0-py3-none-any.whl
- Upload date:
- Size: 8.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9ec3d9e15cce3b9060919ab5edc11121f6827c1bbecd809a8599308af91a3d90
|
|
| MD5 |
bc2bbab4bd993de056bb57cca26f177a
|
|
| BLAKE2b-256 |
5d52a61d1479af000347d422244aabef085420050c0f2ce827a0fc5b9ad5b563
|
Provenance
The following attestation bundles were made for py_keyof-1.1.0-py3-none-any.whl:
Publisher:
ci.yml on eyusd/keyof
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
py_keyof-1.1.0-py3-none-any.whl -
Subject digest:
9ec3d9e15cce3b9060919ab5edc11121f6827c1bbecd809a8599308af91a3d90 - Sigstore transparency entry: 1567259354
- Sigstore integration time:
-
Permalink:
eyusd/keyof@1786850c1edaf74905b722fbcb5f7143f5e48475 -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/eyusd
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@1786850c1edaf74905b722fbcb5f7143f5e48475 -
Trigger Event:
release
-
Statement type: