Skip to main content

Type-safe property path navigation for Python

Project description

keyof

PyPI version Python versions License CI

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:

  1. At runtime, the lambda receives a proxy object that records attribute accesses
  2. 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 default parameter.

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

py_keyof-1.1.0.tar.gz (12.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

py_keyof-1.1.0-py3-none-any.whl (8.7 kB view details)

Uploaded Python 3

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

Hashes for py_keyof-1.1.0.tar.gz
Algorithm Hash digest
SHA256 664783a538df7275a8c41bf8dade558957c057a1660980220d88f3660acd517c
MD5 3813873941156a19a66962186215cc4f
BLAKE2b-256 b623d8df12249fec07809b7175fba5f4ca0d79285f57f457788dbe26df455502

See more details on using hashes here.

Provenance

The following attestation bundles were made for py_keyof-1.1.0.tar.gz:

Publisher: ci.yml on eyusd/keyof

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for py_keyof-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9ec3d9e15cce3b9060919ab5edc11121f6827c1bbecd809a8599308af91a3d90
MD5 bc2bbab4bd993de056bb57cca26f177a
BLAKE2b-256 5d52a61d1479af000347d422244aabef085420050c0f2ce827a0fc5b9ad5b563

See more details on using hashes here.

Provenance

The following attestation bundles were made for py_keyof-1.1.0-py3-none-any.whl:

Publisher: ci.yml on eyusd/keyof

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page