A lightweight Python utility library for common mathematical, algorithmic, and functional tasks.
- Install
- Quick Start
- Functions Overview
- API Reference
- Disclaimer
- Contributing
- Issues & Bug Reports
- License
pip install -U funcboxor
python -m pip install -U funcboxpip install git+https://github.com/funcBox-i3/funcBox.gitor
python -m pip install git+https://github.com/funcBox-i3/funcBox.git- Python 3.8+ - FuncBox is compatible with Python 3.8 and newer versions.
- No external dependencies - FuncBox is a lightweight library with zero external dependencies, using only Python's standard library.
from funcbox import *
is_prime(17)
# True
classify_numbers([2, 3, 4, 5, 6])
# {'primes': [2, 3, 5], 'composites': [4, 6], 'neither': []}
d = Dig({"user": {"name": "Aditya Prasad S", "handle": "Pu94X", "age": 22}})
d("user.name")
# 'Aditya Prasad S'
d(["user.name", "user.handle", "user.age"])
# ['Aditya Prasad S', 'Pu94X', 22]Important
Functions marked as Beta are under active development and their API - including parameter names, return types, and behaviour - may change at any time before a stable release.
| Function | Description | Status |
|---|---|---|
| binary_search | Searches for a value in a sorted sequence | Published |
| dijkstra | Calculates shortest paths in a graph using Dijkstra's algorithm | Published |
| knapsack | Solves the 0/1 knapsack optimisation problem | Published |
| Function | Description | Status |
|---|---|---|
| classify_numbers | Categorizes integers into prime, composite, and neutral subsets | Published |
| fibonacci | Computes the |
Published |
| get_factors | Computes all proper divisors of an integer | Published |
| is_prime | Determines whether a given integer is prime | Published |
| primes | Generates primes within a range via the Sieve of Eratosthenes | Published |
| Function | Description | Status |
|---|---|---|
| chunk | Splits an iterable into consecutive fixed-size chunks | Published |
| clamp | Clamps a number to an inclusive [lo, hi] range |
Published |
| deep_merge | Recursively merges two dicts, preserving nested keys | Published |
| flatten | Flattens a nested iterable to a single list | Published |
| fuzzy_search | Ranks candidates by fuzzy similarity to a query string | Published |
| group_by | Groups iterable elements by a key function or attribute name | Published |
| is_anagram | Checks whether two strings are anagrams of each other | Published |
| is_null_or_blank | Returns True if a value is None, a whitespace-only string, or an empty collection |
Published |
| levenshtein_distance | Returns the Levenshtein edit distance between two strings | Published |
| similarity | Scores the fuzzy similarity between two strings | Published |
| truncate | Shortens a string to a maximum length, appending a suffix | Published |
| Function | Description | Status |
|---|---|---|
| dig | Wraps a nested object (dict, list, or tuple) for safe, repeated dot-path lookups | Published |
Tip
Functions are organized by category below. Each function includes its signature, parameters, return type, and practical examples.
Searches for a target value in a sorted sequence.
binary_search(arr: Sequence, target: Any) -> intParameters
arr(Sequence): A sorted sequence to search through (e.g.list,tuple,range).target(Any): The value to search for.
Returns
int: The index of the target if found,-1otherwise.
Raises
TypeError: Raised ifarris not aSequence.
Examples
from funcbox import binary_search
print(binary_search([1, 3, 5, 7, 9], 7))
# 3
print(binary_search([1, 3, 5, 7, 9], 4))
# -1Calculates the shortest paths from a source node to all other reachable nodes in a weighted graph using Dijkstra's algorithm.
dijkstra(graph: dict, start_node: Any, end_node: Any = None) -> dictParameters
graph(dict): An adjacency list where each node maps to adictof{neighbor: weight}pairs. All weights must be non-negative numbers and all neighbor keys must be nodes in the graph.start_node: The origin node for pathfinding computation.end_node: Optional terminal node. If provided, the algorithm terminates early once the shortest path to this node is found.
Raises
ValueError: Raised ifgraphis not adict, any node's adjacency value is not adict, any neighbor key is not present in the graph, any edge weight is not a number or is negative,start_nodeis not in the graph, orend_nodeis specified but not in the graph.
Returns
dict: A resultant dictionary comprised of two objects:'distances': The calculated minimum distances from thestart_nodeto all resolved nodes. Unreachable nodes evaluate to positive infinity (float('inf')).'paths': Ordered sequences of nodes representing the shortest path from thestart_node. Unreachable nodes map toNone.
Examples
from funcbox import dijkstra
from pprint import pprint
graph = {
'A': {'B': 4, 'C': 2},
'B': {'D': 5, 'E': 1},
'C': {'B': 1, 'E': 3},
'D': {'F': 2},
'E': {'D': 1, 'F': 4},
'F': {}
}
result = dijkstra(graph, 'A')
pprint(result['distances'])
# {'A': 0, 'B': 3, 'C': 2, 'D': 5, 'E': 4, 'F': 7}
pprint(result['paths'])
# {'A': ['A'],
# 'B': ['A', 'C', 'B'],
# 'C': ['A', 'C'],
# 'D': ['A', 'C', 'B', 'E', 'D'],
# 'E': ['A', 'C', 'B', 'E'],
# 'F': ['A', 'C', 'B', 'E', 'D', 'F']}
result = dijkstra(graph, 'A', 'F')
print(result['distances']['F'])
# 7
print(result['paths']['F'])
# ['A', 'C', 'B', 'E', 'D', 'F']Recursively merge override into base, returning a new dict. Unlike {**base, **override} (shallow merge), this descends into nested dicts so deeply nested keys are merged rather than overwritten.
deep_merge(base: dict, override: dict) -> dictParameters
base(dict): The starting dictionary.override(dict): Dictionary whose values take precedence. Keys absent frombaseare added.
Returns
dict: A new merged dict. Neither input is mutated.
Raises
TypeError: If either argument is not adict.
Examples
from funcbox import deep_merge
base = {"db": {"host": "localhost", "port": 5432}, "debug": False}
override = {"db": {"port": 5433, "name": "prod"}, "debug": True}
result = deep_merge(base, override)
# {'db': {'host': 'localhost', 'port': 5433, 'name': 'prod'}, 'debug': True}
# Shallow merge (built-in) would have lost 'host':
# {**base, **override} => {'db': {'port': 5433, 'name': 'prod'}, 'debug': True} ← 'host' gone!Group elements of iterable by a key function or string attribute. Unlike itertools.groupby, no pre-sorting is required — a single pass collects all groups.
group_by(iterable: Iterable, key: callable | str) -> dictParameters
iterable: Any iterable of items.key: Either a callable (invoked on each item) or a string used as adictkey or object attribute.
Returns
dict: Maps each distinct key value to a list of items that produced it. First-seen key order is preserved.
Raises
TypeError: Ifiterableis not iterable orkeyis not a str or callable.KeyError: If a stringkeyis not found on an item.
Examples
from funcbox import group_by
# Group by first letter
words = ["apple", "ant", "banana", "bear", "cherry"]
group_by(words, lambda w: w[0])
# {'a': ['apple', 'ant'], 'b': ['banana', 'bear'], 'c': ['cherry']}
# Group dicts by a string key
people = [{"name": "Alice", "dept": "Eng"}, {"name": "Bob", "dept": "HR"},
{"name": "Carol", "dept": "Eng"}]
group_by(people, "dept")
# {'Eng': [{'name': 'Alice', ...}, {'name': 'Carol', ...}], 'HR': [{'name': 'Bob', ...}]}
# Group numbers by remainder
group_by(range(10), lambda n: n % 3)
# {0: [0, 3, 6, 9], 1: [1, 4, 7], 2: [2, 5, 8]}Solves the 0/1 knapsack problem: select a subset of items to maximise total value while keeping total weight ≤ capacity. Each item may be chosen at most once. Uses a space-optimised 1-D rolling DP array so memory is O(capacity) rather than O(n × capacity).
knapsack(weights: list[int], values: list[int | float], capacity: int) -> dictParameters
weights(list[int]): Non-negative integer weight of each item.values(list[int | float]): Non-negative numeric value of each item.capacity(int): Maximum total weight allowed.
Returns
dictwith keys:'max_value'– maximum achievable value.'selected'– 0-based indices of chosen items.'total_weight'– combined weight of selected items.
Raises
TypeError: If inputs are not the correct types.ValueError: If lists have unequal length, capacity is negative, or any weight/value is negative.
Examples
from funcbox import knapsack
result = knapsack([2, 3, 4, 5], [3, 4, 5, 6], capacity=8)
print(result["max_value"]) # 10
print(result["selected"]) # [1, 2] (0-indexed)
print(result["total_weight"]) # 7Sort arr using a bottom-up iterative merge sort — no recursion, no call-stack pressure. Time O(n log n), space O(n).
merge_sort(arr: list, *, key=None, reverse: bool = False) -> listParameters
arr(list): The list to sort. Not modified in place.key(callable | None): Key extraction function, same semantics assorted().reverse(bool): IfTrue, sort descending.
Returns
list: A new sorted list.
Raises
TypeError: Ifarris not a list.
Examples
from funcbox import merge_sort
print(merge_sort([3, 1, 4, 1, 5, 9]))
# [1, 1, 3, 4, 5, 9]
print(merge_sort([3, 1, 4], reverse=True))
# [4, 3, 1]
print(merge_sort(["banana", "apple", "fig"], key=len))
# ['fig', 'apple', 'banana']Sort arr using 3-way introsort with median-of-three pivot selection and an insertion-sort fallback for small slices (≤ 16 elements). Average O(n log n), worst-case mitigated by pivot strategy, space O(log n).
quick_sort(arr: list, *, key=None, reverse: bool = False) -> listParameters
arr(list): The list to sort. Not modified in place.key(callable | None): Key extraction function.reverse(bool): IfTrue, sort descending.
Returns
list: A new sorted list.
Raises
TypeError: Ifarris not a list.
Examples
from funcbox import quick_sort
print(quick_sort([3, 1, 4, 1, 5, 9]))
# [1, 1, 3, 4, 5, 9]
print(quick_sort(["cherry", "apple", "fig"], key=len, reverse=True))
# ['cherry', 'apple', 'fig']Categorizes a sequence of integers into prime, composite, and neutral sets (0, 1, or negative numbers).
classify_numbers(numbers: list[int]) -> dict[str, list[int]]Parameters
numbers(list[int]): A list of integers to categorize. All elements must be plain integers.
Raises
TypeError: Raised ifnumbersis not a list, or if any element is not a plain integer.
Returns
dict: A dictionary containing three lists:'primes': Integers that are prime.'composites': Integers that are composite (greater than 1 and not prime).'neither': Integers that are neither prime nor composite (< 2).
Examples
from funcbox import classify_numbers
print(classify_numbers([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]))
# {'primes': [2, 3, 5, 7], 'composites': [4, 6, 8, 9], 'neither': [0, 1]}
print(classify_numbers([-5, 0, 1, 13, 15]))
# {'primes': [13], 'composites': [15], 'neither': [-5, 0, 1]}Computes Fibonacci sequence values. Supports retrieving an individual
fibonacci(n: int, output_type: str = "int") -> int | list[int]Parameters
-
n(int): The sequence index (0-indexed) or the total count of elements to generate. Must be a plain integer. -
output_type(str): Specification for the return format.-
"int"(default): Returns a single integer corresponding to the$n$ -th term. -
"list": Returns a list consisting of the firstnterms.
-
Returns
intifoutput_typeis"int".list[int]ifoutput_typeis"list".
Raises
TypeError: Raised ifnis not a plain integer oroutput_typeis not a string.ValueError: Raised ifnis a negative integer or if an unsupportedoutput_typeis provided.
Examples
from funcbox import fibonacci
print(fibonacci(0))
# 0
print(fibonacci(5))
# 5
print(fibonacci(5, output_type="list"))
# [0, 1, 1, 2, 3]Computes all proper divisors (factors) of an integer, excluding the number itself.
get_factors(num: int) -> list[int]Parameters
num(int): The target integer to compute factors for. Must be a plain integer (not aboolorfloat).
Raises
TypeError: Raised ifnumis not a plain integer.
Returns
list[int]: A sorted list of all proper factors.
Examples
from funcbox import get_factors
print(get_factors(12)) # [1, 2, 3, 4, 6]
print(get_factors(7)) # [1]Determines whether a given integer is prime.
is_prime(n: int) -> boolParameters
n(int): The integer to evaluate. Must be a plain integer (not aboolorfloat).
Raises
TypeError: Raised ifnis not a plain integer.
Returns
bool:Trueif the integer is prime,Falseotherwise.
Examples
from funcbox import is_prime
print(is_prime(7))
# True
print(is_prime(10))
# FalseGenerates a sequence of prime numbers within a specified bounds utilizing the Sieve of Eratosthenes algorithm.
primes(start: int = 2, limit: int) -> list[int]Parameters
start(int): The inclusive lower bound for prime generation. Defaults to 2.limit(int): The inclusive upper bound for prime generation.
Returns
list[int]: An ordered list of prime numbers fromstartboundary up to the specifiedlimit.
Raises
TypeError: Raised ifstartorlimitis not a plain integer.ValueError: Raised if eitherlimitorstartare evaluated to be less than 2.
Examples
from funcbox import primes
print(primes(limit=10))
# [2, 3, 5, 7]
print(primes(start=10, limit=20))
# [11, 13, 17, 19]Split iterable into consecutive chunks of at most size elements. The final chunk may be smaller.
chunk(iterable: Iterable, size: int) -> list[list]Parameters
iterable: Any iterable to split.size(int): Maximum elements per chunk. Must be a positive integer.
Returns
list[list]: List of chunks, each of length ≤size.
Raises
TypeError: Ifiterableis not iterable orsizeis not a plain int.ValueError: Ifsizeis not positive.
Examples
from funcbox import chunk
print(chunk([1, 2, 3, 4, 5], 2))
# [[1, 2], [3, 4], [5]]
print(chunk(range(6), 3))
# [[0, 1, 2], [3, 4, 5]]Return value clamped to the inclusive range [lo, hi].
clamp(value: int | float, lo: int | float, hi: int | float) -> int | floatParameters
value: The number to clamp.lo: Inclusive lower bound.hi: Inclusive upper bound.
Returns
- The clamped value (
loif below,hiif above,valueotherwise).
Raises
TypeError: If any argument is not a real number.ValueError: Iflo > hi.
Examples
from funcbox import clamp
print(clamp(5, 1, 10)) # 5
print(clamp(-3, 0, 100)) # 0
print(clamp(150, 0, 100)) # 100
print(clamp(3.7, 0.0, 5.0)) # 3.7Recursively flatten a nested iterable into a single list. Strings are treated as atomic and are never character-iterated. Uses an iterative DFS stack instead of recursion, so deeply nested structures don't hit Python's call-stack limit.
flatten(nested: Iterable, depth: int | None = None) -> listParameters
nested: The iterable to flatten.depth(int | None): Maximum nesting depth to flatten.None= fully flatten.
Returns
list: A flat list of elements.
Raises
TypeError: Ifnestedis not iterable ordepthis not a positive int.ValueError: Ifdepthis provided but not a positive integer.
Examples
from funcbox import flatten
print(flatten([1, [2, [3, [4]]]]))
# [1, 2, 3, 4]
print(flatten([1, [2, [3]]], depth=1))
# [1, 2, [3]]
print(flatten([1, "ab", [2, "cd"]]))
# [1, 'ab', 2, 'cd']Finds the best fuzzy matches for query within candidates, scoring each item by a blend of three signals: OSA edit-distance similarity (weight 0.5), ordered subsequence coverage (weight 0.3), and partial-window ratio (weight 0.2). Results are sorted best-first. This function has zero external dependencies.
fuzzy_search(
query: str,
candidates: Sequence[Any],
*,
threshold: float = 0.0,
limit: int | None = None,
key: Callable | None = None,
) -> list[dict[str, Any]]Parameters
query(str): The search string.candidates(Sequence): Items to search through. Must be strings, or use key for arbitrary objects.threshold(float): Minimum score (inclusive) in[0.0, 1.0]. Candidates scoring below this are excluded. Defaults to0.0.limit(int | None): Maximum number of results to return.Nonereturns all matches above the threshold.key(callable | None): Extracts the comparison string from each candidate. WhenNone, candidates must be strings.
Returns
list[dict]: A list of dicts sorted by'score'descending, each containing:'match'– the original candidate item.'score'– similarity score as afloatin[0.0, 1.0].
Raises
TypeError: Ifqueryis not astr,candidatesis not aSequence(or is a barestr),keyis not callable (when provided), or any candidate is not astrwhen no key is given.ValueError: Ifthresholdis outside[0.0, 1.0], orlimitis not a positive integer.
Examples
from funcbox import fuzzy_search
# Basic string search
fuzzy_search("pyth", ["Python", "Ruby", "Rust", "PyPy"])
# [{'match': 'Python', 'score': 0.8333}, {'match': 'PyPy', 'score': 0.75}, ...]
# Tolerate typos
fuzzy_search("dijktra", ["dijkstra", "binary search", "bubble sort"])
# [{'match': 'dijkstra', 'score': 0.8804}, ...]
# Filter by minimum score
fuzzy_search("rust", ["Python", "Ruby", "Rust"], threshold=0.5)
# [{'match': 'Rust', 'score': 1.0}]
# Limit results
fuzzy_search("py", ["Python", "PyPy", "Ruby", "Rust"], limit=2)
# [{'match': 'PyPy', 'score': ...}, {'match': 'Python', 'score': ...}]
# Search objects with a key function
people = [{"name": "Alice"}, {"name": "Alicia"}, {"name": "Bob"}]
fuzzy_search("alic", people, key=lambda p: p["name"])
# [{'match': {'name': 'Alice'}, 'score': ...}, {'match': {'name': 'Alicia'}, 'score': ...}]
# Score meaning: 1.0 = exact match, 0.0 = completely dissimilar
fuzzy_search("hello", ["hello", "helo", "world"])
# [{'match': 'hello', 'score': 1.0}, {'match': 'helo', 'score': 0.85}, {'match': 'world', 'score': 0.1}]Scores the fuzzy similarity between two strings using a blend of three signals: OSA edit-distance ratio (weight 0.5), ordered subsequence coverage (weight 0.3), and partial-window ratio (weight 0.2). OSA distance correctly treats transpositions of adjacent characters as a single edit, improving accuracy over plain Levenshtein for common typos. This is the same scoring function used internally by fuzzy_search, exposed for cases where you want to score a single pair directly.
similarity(query: str, candidate: str) -> floatParameters
query(str): The reference string (e.g. the user's search term).candidate(str): The string to score against query.
Returns
float: A score in[0.0, 1.0].1.0means identical (case-insensitively);0.0means completely dissimilar.
Raises
TypeError: If either argument is not astr.
Examples
from funcbox import similarity
print(similarity("hello", "hello"))
# 1.0
print(similarity("pyth", "Python")) # partial prefix
# 0.8333
print(similarity("dijktra", "dijkstra")) # typo tolerance
# 0.8804
print(similarity("pytohn", "python")) # transposition — 1 edit (OSA)
# 0.7833
print(similarity("search", "fuzzy_search")) # substring in longer string
# 0.75
print(similarity("abc", "xyz")) # no overlap
# 0.0
# Sort a list manually by score
words = ["Python", "PyPy", "Ruby", "Rust"]
words.sort(key=lambda w: similarity("pyth", w), reverse=True)
print(words)
# ['Python', 'PyPy', 'Rust', 'Ruby']Returns the Levenshtein edit distance between two strings — i.e., the minimum number of single-character insertions, deletions, and substitutions required to transform a into b. Transpositions of adjacent characters count as two edits (one deletion + one insertion). For transposition-aware distance, see similarity which uses OSA distance internally.
levenshtein_distance(a: str, b: str) -> intParameters
a(str): First string.b(str): Second string.
Returns
int: Non-negative edit distance.0means the strings are identical.
Raises
TypeError: If either argument is not astr.
Examples
from funcbox import levenshtein_distance
print(levenshtein_distance("kitten", "sitting"))
# 3
print(levenshtein_distance("hello", "hello"))
# 0
print(levenshtein_distance("dijktra", "dijkstra")) # 1 substitution
# 1
print(levenshtein_distance("", "abc"))
# 3Checks if two strings are anagrams of each other.
is_anagram(str1: str, str2: str, case: bool = False, spaces: bool = False, punct: bool = False) -> boolParameters
str1(str): First string to compare.str2(str): Second string to compare.case(bool): Ignore case when comparing. Defaults toFalse.spaces(bool): Ignore spaces when comparing. Defaults toFalse.punct(bool): Ignore punctuation when comparing. Defaults toFalse.
Raises
TypeError: Raised ifstr1orstr2is not a string.
Returns
bool:Trueif the strings are anagrams,Falseotherwise.
Examples
from funcbox import is_anagram
print(is_anagram("listen", "silent"))
# True
print(is_anagram("Listen", "Silent", case=True))
# True
print(is_anagram("a gentleman", "elegant man", spaces=True))
# True
print(is_anagram("Astronomer!", "Moon starer", case=True, punct=True, spaces=True))
# True
print(is_anagram("hello", "world"))
# FalseReturns True if value is None, a whitespace-only string (or empty string), or an empty collection. Emptiness for collections is detected via len(), which is O(1) for all built-in types. Returns False for non-empty collections, non-None non-Sized types, and strings with at least one non-whitespace character.
is_null_or_blank(value: object) -> boolParameters
value(object): Any value.None,str, and anySized(e.g.list,dict,tuple,set,frozenset,bytes,bytearray, or custom classes implementing__len__) are all evaluated.
Returns
-
bool:Trueifvalueis:None- An empty string (
"") or a string of only whitespace. - Any empty
Sized(i.e.len(value) == 0).
Falseotherwise.
Examples
from funcbox import is_null_or_blank
print(is_null_or_blank(None))
# True
print(is_null_or_blank("\t\n"))
# True
print(is_null_or_blank([]))
# True
print(is_null_or_blank([1, 2]))
# False
print(is_null_or_blank({}))
# TrueShortens text to at most max_length characters (including the suffix). If the text already fits, it is returned unchanged.
truncate(text: str, max_length: int, suffix: str = "...", *, word_boundary: bool = False) -> strParameters
text(str): The source string to truncate.max_length(int): Maximum total length of the result, including the suffix. Must be a positive integer ≥len(suffix).suffix(str): Appended after the cut. Defaults to"...".word_boundary(bool): WhenTrue, the cut snaps back to the last whitespace so words are never split. Defaults toFalse.
Returns
str: The original string if it fits, otherwise the truncated string with the suffix appended.
Raises
TypeError: Raised iftextorsuffixis not astr, ormax_lengthis not a plainint.ValueError: Raised ifmax_lengthis not positive or is shorter thansuffix.
Examples
from funcbox import truncate
print(truncate("Hello, world!", 8))
# 'Hello...'
print(truncate("Hello, world!", 13)) # fits — returned unchanged
# 'Hello, world!'
print(truncate("Hello, world!", 10, suffix="…"))
# 'Hello, wo…'
print(truncate("The quick brown fox", 12, word_boundary=True))
# 'The quick...'
print(truncate("The quick brown fox", 12)) # no word boundary
# 'The quick...'Wraps a nested dictionary once and lets you query it repeatedly using dot-path strings, explicit key sequences, or multi-path batches - all through a single, consistent interface.
dig(data: dict[str, Any])Parameters
data(dict[str, Any]): The source dictionary to wrap.
Raises
TypeError: Raised ifdatais not adict.
Path types
Choose your path format based on your needs:
str(dot-path): Most cases; numbers auto-convert to list indices (e.g.,"user.address.city"or"projects.0.name")tuple: Keys with dots, or explicit integer indices (e.g.,("user", "projects", 0, "name"))list: Query multiple paths in one call (e.g.,["user.name", "user.age"])
Methods and operations
d(path): Get value at path, returnsNoneif not foundd(path, default=x): Get value, returnxif path failsd(path, last=True): Return the deepest found value before a missd([paths...]): Resolve multiple paths → returns an orderedlist(same order as input; missing paths yieldNoneordefault)d[path]: Shorthand ford(path)path in d: Check if path exists (True even if value isNone)d.scope(path): Create a new Dig rooted at that sub-path
Raises (on lookup)
TypeError: Ifpathis not astr,tuple, orlist; or if a multi-path list entry is not astrortuple.KeyError: Ifpathpassed toscope()does not exist.TypeError: If the value atpathpassed toscope()is not adict.
Returns
Any- the resolved value for single-path lookups.list[Any]- values aligned position-for-position with the input paths for multi-path lookups; missing paths yieldNone(ordefault).dig- a new scoped instance when callingscope().
Examples
from funcbox import Dig
data = {
"user": {
"name": "Aditya Prasad S",
"handle": "Pu94X",
"age": 19,
"email": None,
"address": {
"city": "Kanyakumari",
"state": "Tamil Nadu",
"zip": "629000",
},
"projects": [
{"name": "funcBox", "stars": 42, "lang": "Python"},
{"name": "InfiniKit", "stars": 18, "lang": "Kotlin"},
],
"settings": {
"theme": "dark",
"notifications": {"email": True, "push": False},
},
}
}
d = Dig(data) # Wrap once, query as many times as you like# Navigate nested dicts with dot notation
d("user.name")
# 'Aditya Prasad S'
d("user.address.city")
# 'Kanyakumari'# Numeric segments in dot-paths become list indices
d("user.projects.0.name")
# 'funcBox'
d("user.projects.1.lang")
# 'Kotlin'# Without default, missing keys return None
d("user.phone")
# None
# With default, use a fallback value
d("user.phone", default="N/A")
# 'N/A'
# last=True returns the deepest value found before the miss
d("user.address.phone", last=True)
# {'city': 'Kanyakumari', 'state': 'Tamil Nadu', 'zip': '629000'}# Useful when keys contain dots, or to avoid string parsing
d(("user", "projects", 0, "stars"))
# 42# Square bracket shorthand for single lookups
d["user.age"]
# 19
# Test if a path exists (True even if value is None)
"user.email" in d
# True (key exists, value is None)
"user.phone" in d
# False (key doesn't exist)# Pass a list of paths to get results as an ordered list
d(["user.name", "user.handle", "user.age"])
# ['Aditya Prasad S', 'Pu94X', 19]
# Missing paths yield None (or the default) at the same position
d(["user.name", "user.phone", "user.age"])
# ['Aditya Prasad S', None, 19]
# Defaults apply to all paths in a multi-path query
d(["user.projects.0.name", "user.projects.1.name"], default="unknown")
# ['funcBox', 'InfiniKit']# Create a scoped Dig at a sub-node
addr = d.scope("user.address")
addr("city") # Query relative to the scope
# 'Kanyakumari'
addr["zip"] # Shorthand works too
# '629000'
addr(["city", "state", "zip"]) # Multi-path relative to scope
# ['Kanyakumari', 'Tamil Nadu', '629000']# Scoping works with list indices too
proj = d.scope("user.projects.0")
proj("name")
# 'funcBox'
proj("stars")
# 42# Scope from a scope for deeply nested access
notif = d.scope("user.settings.notifications")
notif("email") # Cleaner than: d("user.settings.notifications.email")
# True
notif("push")
# False# See the top-level keys of a Dig
repr(d)
# Dig({'user'})
# After scoping, see the keys at that level
addr = d.scope("user.address")
repr(addr)
# Dig({'city', 'state', 'zip'})Note
JSON integration: Dig only accepts Python dict objects. If you're working with JSON, parse it first using json.loads() or json.load():
import json
from funcbox import Dig
json_string = '{"user": {"name": "Aditya"}}'
data = json.loads(json_string) # Parse to dict first
d = Dig(data)
d("user.name") # 'Aditya'Note
Numeric string segments like "0" in a dot-path are automatically coerced to integer indices when the current node is a list or tuple. Use a tuple path with an int element (e.g. ("user", "projects", 0, "name")) for unambiguous index access.
FuncBox is provided as-is for general use. The developers make no warranties or guarantees regarding its fitness for any particular purpose. Users are responsible for validating results for their specific use cases and testing thoroughly before production deployment.
Contributions are welcome! Fork the repository, make your changes, and submit a pull request. Please ensure contributions align with the project's coding standards and include appropriate documentation.
Found a bug or have a feature request? Open an issue on GitHub. Please search existing issues first to avoid duplicates and provide a clear description with reproducible code. For security vulnerabilities, email the maintainers directly.
Licensed under the MIT License. See LICENSE for details.
Copyright © 2026