Skip to main content

Daisies

Daisies is a Python library that provides easy, safe navigation for complex objects.

from daisies import Chain

raw = {
    "never": {
        "gonna": {
            "give": {
                "you": {
                    "up": "never gonna let you down"
                }
            }
        }
    },
    "artists": [
        {
            "name": "Rick Astley",
            "genre": "Pop"
        },
        {
            "name": "Michael Jackson",
            "genre": "Pop"
        }
    ]
}
data = Chain(raw)


# Accessing nested dict keys that may or may not exist
print(data.never.gonna.give.you.up)  # "never gonna let you down"
print(data.let.you.down)  # None

# Navigating through lists
print(data.artists[0].name)  # "Rick Astley"
print(data.artists[1].name)  # "Michael Jackson"
print(data.artists[2].name)  # None

Installation and Support

Daisies is available on PyPI, so you can install it like any other Python package, using the packager of your choice.

pip install daisies

Daisies is automatically tested with 100% coverage on Python 3.10 and above for Ubuntu, macOS, and Windows.

Why Daisies?

Daisies makes data navigation easy and safe so you don't have to constantly null-check, coalesce, try/except, and if/else.

You want data? Just go get it.

Simply wrap your raw data in a Chain object, which allows you to access its attributes using the dot notation. If a key, attribute, or list index does not exist, the Chain object will return None instead of raising an exception.

This is particularly useful when dealing with complex data structures, such as JSON responses from APIs, where you can't always guarantee the presence of every key or index.

Usage

Daisies aims to be as simple and intuitive as possible, so you can use it without having to write tons of code for null-checking and type validation.

For the most part, you can just use Daisies and pretend that you're working with the raw data directly. However, there are some important differences to be aware of, especially when working with scalar values, arithmetic operations, and identity comparisons.

Usage: Scalar values

Daisies allows you to seamlessly work with scalar values, such as strings, integers, and Booleans. This allows you to operate on your data naturally.

data = Chain({
    "name": "John Doe",
    "age": 30,
    "is_active": True
})

print(data.name)  # "John Doe"
print(data.age)  # 30
print(data.is_active)  # True

You can operate at any point on the Chain object, and it will return the expected result.

data = Chain({
    "name": "John Doe",
    "age": 30,
    "is_active": True
})

print(data.name.upper())  # "JOHN DOE"
print(data.age + 10)  # 40

Usage: Numeric values and arithmetic operations

Daisies allows you to work with numeric values and perform arithmetic operations on them, but certain operations work differently from usual to ensure that you don't raise errors, such as coercing None values to zero.

data = Chain({
    "price": 100,
    "quantity": 5
})

print(data.price * data.quantity)  # 500
print(data.missing + 10)  # 10
print(data.quantity ** 3)  # 125

# Chain even allows division by zero, returning 0 instead.
# Not even Stephen Hawking could do that.
print(data.missing / 0)  # 0

Usage: Lists and other iterables

Daisies allows you to work with lists, sets, and other iterables in a natural way. You can access items by index and iterate over them. If they don't exist, Daisies will return None.

Iterating a Chain yields Chain-wrapped items, so you can keep navigating each element safely inside the loop — no need to unwrap and re-wrap. Missing fields on an element still resolve to None instead of raising.

data = Chain({
    "names": ["Alice", "Bob", "Charlie"],
    "users": [{"name": "Alice"}, {"name": "Bob"}]
})

print(data.names[0])  # "Alice"
print(data.names[50])  # None

# Each item is a Chain, so nested access keeps working through the loop:
for user in data.users:
    print(user.name)  # "Alice", then "Bob"

Working with dicts and nested data

Daisies allows you to work with nested data structures quickly and easily. No more null-checking or catching KeyErrors.

data = Chain({
    "user": {
        "name": "Alice",
        "address": {
            "city": "Wonderland",
            "country": "Fairyland"
        }
    }
})

print(data.user.name)  # "Alice"
print(data.user.address.city)  # "Wonderland"
print(data.this.is.missing)  # None

Usage: Typed defaults with .value()

For most uses, calling a Chain with () is enough to unwrap it. But when you want a fallback for missing data — or you want to coerce the value to a particular type — use .value():

data = Chain({
    "user": {
        "role": None,
        "age": "30",
    }
})

# Fallback for missing or None values
print(data.user.role.value(default="guest"))  # "guest"
print(data.user.missing.value(default="n/a"))  # "n/a"

# Coerce to a type, with a typed fallback
print(data.user.age.value(int, default=0))  # 30
print(data.user.missing.value(int, default=0))  # 0

# Coercion failures fall back too — never raises
print(data.user.role.value(int, default=-1))  # -1

This replaces the common ... or "default" pattern at the end of a chain. Because the default's type pins the return type, your IDE and type checker can infer it correctly when the chain is annotated.

Usage: Missing values and .fallback()

Use .exists() and .is_missing() when you need to distinguish a failed lookup from a value that is present but falsy—or explicitly None:

data = Chain({
    "user": {"nickname": None},
    "contact": {"email": "ada@example.com"},
})

print(data.user.nickname.exists())  # True: the key is present
print(data.user.email.is_missing())  # True: the key is absent

.fallback() chooses another Chain or a plain value only when navigation is missing, and keeps the result wrapped:

email = data.user.email.fallback(data.contact.email).fallback("noreply@example.com")
print(email)  # "ada@example.com"

# An explicit None is still a resolved value, so it does not fall back.
print(data.user.nickname.fallback("anonymous").value())  # None

.value(default=...) continues to treat both missing values and explicit None as reasons to use its terminal default. Use .fallback() when the distinction matters.

Usage: Getting data back out with .json(), .dict(), and .list()

Once you've navigated to the part of a payload you care about, you usually want to send it back out — re-serialize it for another API, log it, cache it, or hand it to a template. Daisies gives you three unwrapping methods so you never have to reach for json.dumps(...) or the private ._wrapped attribute.

data = Chain({
    "user": {
        "name": "Alice",
        "roles": ["admin", "editor"],
    }
})

# .json() serializes to a JSON string; kwargs are forwarded to json.dumps
print(data.user.json())  # '{"name": "Alice", "roles": ["admin", "editor"]}'
print(data.user.json(indent=2))  # pretty-printed

# .dict() and .list() return plain, unwrapped containers
print(data.user.dict())  # {"name": "Alice", "roles": ["admin", "editor"]}
print(data.user.roles.list())  # ["admin", "editor"]

True to the rest of the library, these never raise on missing or mismatched data. A missing value serializes to JSON null, and .dict() / .list() return an empty container when the data isn't the shape you asked for:

print(data.missing.json())  # "null"
print(data.missing.dict())  # {}
print(data.user.name.list())  # [] — a string isn't list-like

.json() extends that promise to values json.dumps doesn't understand — a datetime, Decimal, or set degrades to its string form instead of raising TypeError. Pass your own default= to override:

from datetime import date

print(Chain({"when": date(2026, 7, 9)}).json())  # '{"when": "2026-07-09"}'

Usage: Dict views with .keys(), .values(), and .items()

Reach a wrapped dict's keys, values, or items as plain lists — null-tolerant, so a missing or non-dict node answers with [] instead of raising:

settings = Chain({"user": {"theme": "dark", "lang": "en"}})
print(settings.user.keys())    # ["theme", "lang"]
print(settings.user.values())  # ["dark", "en"]
print(settings.user.items())   # [("theme", "dark"), ("lang", "en")]
print(settings.user.missing.keys())  # [] — never raises

A field literally named keys/values/items still wins on attribute access (data.items[0] navigates the field); reach the view via the method call on a dict that doesn't have that key.

Usage: Absent vs. falsy with .exists() and .is_missing()

if data.count: can't tell a genuine 0 from a missing key. .exists() and .is_missing() can — a present-but-falsy value (0, "", [], False) exists; an absent one doesn't:

data = Chain({"count": 0})
print(data.count.exists())      # True — present, even though it's 0
print(data.missing.exists())    # False
print(data.missing.is_missing())  # True

(A key set literally to None reads as missing, since a missing hop is itself None.)

Usage: Inspecting shape with .tree()

When you're handed an unfamiliar payload, print its shape before writing any navigation. .tree() shows keys, value types, and list lengths — tuned for exploration rather than dumping every value — and uses the first element as a representative for lists of objects.

data = Chain({
    "user": {"name": "Ada", "age": 36},
    "roles": ["admin", "editor"],
})

print(data.tree())
# dict
# ├─ user: dict
# │  ├─ name: str = 'Ada'
# │  └─ age: int = 36
# └─ roles: list[2] of str (e.g. 'admin')

It returns a string — so you can log it or paste it into a bug report — and never raises. For large payloads, cap the output with max_depth and max_items. There's also a module-level daisies.tree(data) shorthand for when you haven't wrapped your data yet.

Usage: Special values and identity comparisons

When you access items through a Chain, it's not directly returning the value, it's returning a Chain wrapping the value. A Chain is a very powerful and dynamic object that allows all sorts of operations on it, but it's not the same as the raw value.

This means there are a few special cases to be aware of. Because values are wrapped in a Chain, you can't use the identity operator is to check if a value is None, True, or False like usual.

But good news! If you want to compare identity, you can call the key like it's a function, and it'll return the raw value.

data = Chain({
    "name": "John Doe",
    "age": 30,
    "is_active": True
})
print(data.is_active is True)  # False :(
print(data.missing.key is None)  # False :(
print(data.is_active() is True)  # True :)
print(data.missing.key() is None)  # True :)

Usage: Navigating reserved words and invalid keys

Certain names are reserved in Python, so if your data contains keys that are reserved words or against the Python grammar, you can still access them by using the square bracket notation. In fact, you never have to use the dot notation if you hate it! Either way Daisies will make sure it's safe navigation.

data = Chain({
    "123": "Hello World!",
    "jeffrey-epstein": "Didn't kill himself"
})

print(data.123)  # SyntaxError :(
print(data["123"])  # "Hello World!"

print(data.jeffrey-epstein)  # SyntaxError :(
print(data["jeffrey-epstein"])  # "Didn't kill himself"

Cookbook

For real-world recipes — parsing a Stripe webhook, walking a paginated REST API, hardening against a flaky third-party service, and exploring an unknown payload with .tree() — see the Cookbook.

Contributing

Feel free to report bugs and suggest features through GitHub Issues, or open a PR for improvements.

Metadata

Release files for daisies 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for daisies 0.4.0
File Size Uploaded Provenance Actions
daisies-0.4.0.tar.gz 14.0 kB With provenance Details for daisies-0.4.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for daisies 0.4.0
File Interpreter ABI Platform Actions
daisies-0.4.0-py3-none-any.whl Python 3 none any Details for daisies-0.4.0-py3-none-any.whl

Total release size: 25.5 kB

Release files / daisies-0.4.0.tar.gz

Download URL daisies-0.4.0.tar.gz
Size 14.0 kB
Tags Source
SHA-256 checksum
How to use checksums
d14d42d42226b93952d2a4b40f805fb4b65a45336e5c8a7ef2c944c5208165b5
BLAKE2b-256 checksum
How to use checksums
20452245d143a405a1db64a638bd245a35779cf1c338e561a6d57aeb8feb7979
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 14, 2026.

Transparency log

Release files / daisies-0.4.0-py3-none-any.whl

Download URL daisies-0.4.0-py3-none-any.whl
Size 11.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
97517f4d173d2471dc0e74ae84474d675604a4a21960353e2b50b0e1b66a865d
BLAKE2b-256 checksum
How to use checksums
4ea933daf61ac4c9d6b44c92a0101a5fa1c53245f9c7a1bd451fedfadaa311d4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 14, 2026.

Transparency log

Security for daisies 0.4.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for daisies-0.4.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 14, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page