Skip to content

funcBox-i3/funcBox

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FuncBox

A lightweight Python utility library for common mathematical, algorithmic, and functional tasks.

License: MIT Python 3.8+ PyPI Version GitHub


Table of Contents

Install

Latest Release:

pip install -U funcbox

or

python -m pip install -U funcbox

Beta Version (Pre-release from GitHub):

pip install git+https://github.com/funcBox-i3/funcBox.git

or

python -m pip install git+https://github.com/funcBox-i3/funcBox.git

Requirements

  • 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.

Quick Start

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]

Functions Overview

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.

Algorithms

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

Number Theory

Function Description Status
classify_numbers Categorizes integers into prime, composite, and neutral subsets Published
fibonacci Computes the $n$-th Fibonacci term or sequence 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

String Processing

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

Data Utilities

Function Description Status
dig Wraps a nested object (dict, list, or tuple) for safe, repeated dot-path lookups Published

API Reference

Tip

Functions are organized by category below. Each function includes its signature, parameters, return type, and practical examples.

Algorithms


binary_search(arr, target)

Searches for a target value in a sorted sequence.

Usage

binary_search(arr: Sequence, target: Any) -> int

Parameters

  • 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, -1 otherwise.

Raises

  • TypeError: Raised if arr is not a Sequence.

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))
# -1

dijkstra(graph, start_node, end_node=None)

Calculates the shortest paths from a source node to all other reachable nodes in a weighted graph using Dijkstra's algorithm.

Usage

dijkstra(graph: dict, start_node: Any, end_node: Any = None) -> dict

Parameters

  • graph (dict): An adjacency list where each node maps to a dict of {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 if graph is not a dict, any node's adjacency value is not a dict, any neighbor key is not present in the graph, any edge weight is not a number or is negative, start_node is not in the graph, or end_node is specified but not in the graph.

Returns

  • dict: A resultant dictionary comprised of two objects:
    • 'distances': The calculated minimum distances from the start_node to all resolved nodes. Unreachable nodes evaluate to positive infinity (float('inf')).
    • 'paths': Ordered sequences of nodes representing the shortest path from the start_node. Unreachable nodes map to None.

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']

deep_merge(base, override)

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.

Usage

deep_merge(base: dict, override: dict) -> dict

Parameters

  • base (dict): The starting dictionary.
  • override (dict): Dictionary whose values take precedence. Keys absent from base are added.

Returns

  • dict: A new merged dict. Neither input is mutated.

Raises

  • TypeError: If either argument is not a dict.

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_by(iterable, key)

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.

Usage

group_by(iterable: Iterable, key: callable | str) -> dict

Parameters

  • iterable: Any iterable of items.
  • key: Either a callable (invoked on each item) or a string used as a dict key 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: If iterable is not iterable or key is not a str or callable.
  • KeyError: If a string key is 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]}

Number Theory


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).

Usage

knapsack(weights: list[int], values: list[int | float], capacity: int) -> dict

Parameters

  • 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

  • dict with 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"]) # 7

merge_sort(arr, *, key=None, reverse=False)

Sort arr using a bottom-up iterative merge sort — no recursion, no call-stack pressure. Time O(n log n), space O(n).

Usage

merge_sort(arr: list, *, key=None, reverse: bool = False) -> list

Parameters

  • arr (list): The list to sort. Not modified in place.
  • key (callable | None): Key extraction function, same semantics as sorted().
  • reverse (bool): If True, sort descending.

Returns

  • list: A new sorted list.

Raises

  • TypeError: If arr is 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']

quick_sort(arr, *, key=None, reverse=False)

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).

Usage

quick_sort(arr: list, *, key=None, reverse: bool = False) -> list

Parameters

  • arr (list): The list to sort. Not modified in place.
  • key (callable | None): Key extraction function.
  • reverse (bool): If True, sort descending.

Returns

  • list: A new sorted list.

Raises

  • TypeError: If arr is 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']

Number Theory


classify_numbers(numbers)

Categorizes a sequence of integers into prime, composite, and neutral sets (0, 1, or negative numbers).

Usage

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 if numbers is 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]}

fibonacci(n, output_type="int")

Computes Fibonacci sequence values. Supports retrieving an individual $n$-th term or an array containing the sequence up to the $n$-th element.

Usage

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 first n terms.

Returns

  • int if output_type is "int".
  • list[int] if output_type is "list".

Raises

  • TypeError: Raised if n is not a plain integer or output_type is not a string.
  • ValueError: Raised if n is a negative integer or if an unsupported output_type is 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]

get_factors(num)

Computes all proper divisors (factors) of an integer, excluding the number itself.

Usage

get_factors(num: int) -> list[int]

Parameters

  • num (int): The target integer to compute factors for. Must be a plain integer (not a bool or float).

Raises

  • TypeError: Raised if num is 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]

is_prime(n)

Determines whether a given integer is prime.

Usage

is_prime(n: int) -> bool

Parameters

  • n (int): The integer to evaluate. Must be a plain integer (not a bool or float).

Raises

  • TypeError: Raised if n is not a plain integer.

Returns

  • bool: True if the integer is prime, False otherwise.

Examples

from funcbox import is_prime

print(is_prime(7))
# True
print(is_prime(10))
# False

primes(start, limit)

Generates a sequence of prime numbers within a specified bounds utilizing the Sieve of Eratosthenes algorithm.

Usage

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 from start boundary up to the specified limit.

Raises

  • TypeError: Raised if start or limit is not a plain integer.
  • ValueError: Raised if either limit or start are 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]

String Processing


chunk(iterable, size)

Split iterable into consecutive chunks of at most size elements. The final chunk may be smaller.

Usage

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: If iterable is not iterable or size is not a plain int.
  • ValueError: If size is 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]]

clamp(value, lo, hi)

Return value clamped to the inclusive range [lo, hi].

Usage

clamp(value: int | float, lo: int | float, hi: int | float) -> int | float

Parameters

  • value: The number to clamp.
  • lo: Inclusive lower bound.
  • hi: Inclusive upper bound.

Returns

  • The clamped value (lo if below, hi if above, value otherwise).

Raises

  • TypeError: If any argument is not a real number.
  • ValueError: If lo > 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.7

flatten(nested, depth=None)

Recursively 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.

Usage

flatten(nested: Iterable, depth: int | None = None) -> list

Parameters

  • 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: If nested is not iterable or depth is not a positive int.
  • ValueError: If depth is 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']

fuzzy_search(query, candidates, *, threshold=0.0, limit=None, key=None)

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.

Usage

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 to 0.0.
  • limit (int | None): Maximum number of results to return. None returns all matches above the threshold.
  • key (callable | None): Extracts the comparison string from each candidate. When None, 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 a float in [0.0, 1.0].

Raises

  • TypeError: If query is not a str, candidates is not a Sequence (or is a bare str), key is not callable (when provided), or any candidate is not a str when no key is given.
  • ValueError: If threshold is outside [0.0, 1.0], or limit is 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}]

similarity(query, candidate)

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.

Usage

similarity(query: str, candidate: str) -> float

Parameters

  • 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.0 means identical (case-insensitively); 0.0 means completely dissimilar.

Raises

  • TypeError: If either argument is not a str.

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']

levenshtein_distance(a, b)

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.

Usage

levenshtein_distance(a: str, b: str) -> int

Parameters

  • a (str): First string.
  • b (str): Second string.

Returns

  • int: Non-negative edit distance. 0 means the strings are identical.

Raises

  • TypeError: If either argument is not a str.

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"))
# 3

is_anagram(str1, str2, case=False, spaces=False, punct=False)

Checks if two strings are anagrams of each other.

Usage

is_anagram(str1: str, str2: str, case: bool = False, spaces: bool = False, punct: bool = False) -> bool

Parameters

  • str1 (str): First string to compare.
  • str2 (str): Second string to compare.
  • case (bool): Ignore case when comparing. Defaults to False.
  • spaces (bool): Ignore spaces when comparing. Defaults to False.
  • punct (bool): Ignore punctuation when comparing. Defaults to False.

Raises

  • TypeError: Raised if str1 or str2 is not a string.

Returns

  • bool: True if the strings are anagrams, False otherwise.

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"))
# False

is_null_or_blank(value)

Returns 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.

Usage

is_null_or_blank(value: object) -> bool

Parameters

  • value (object): Any value. None, str, and any Sized (e.g. list, dict, tuple, set, frozenset, bytes, bytearray, or custom classes implementing __len__) are all evaluated.

Returns

  • bool: True if value is:

    • None
    • An empty string ("") or a string of only whitespace.
    • Any empty Sized (i.e. len(value) == 0).

    False otherwise.

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({}))
# True

truncate(text, max_length, suffix="...", *, word_boundary=False)

Shortens text to at most max_length characters (including the suffix). If the text already fits, it is returned unchanged.

Usage

truncate(text: str, max_length: int, suffix: str = "...", *, word_boundary: bool = False) -> str

Parameters

  • 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): When True, the cut snaps back to the last whitespace so words are never split. Defaults to False.

Returns

  • str: The original string if it fits, otherwise the truncated string with the suffix appended.

Raises

  • TypeError: Raised if text or suffix is not a str, or max_length is not a plain int.
  • ValueError: Raised if max_length is not positive or is shorter than suffix.

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...'

Data Utilities


dig(data)

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.

Usage

dig(data: dict[str, Any])

Parameters

  • data (dict[str, Any]): The source dictionary to wrap.

Raises

  • TypeError: Raised if data is not a dict.

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, returns None if not found
  • d(path, default=x): Get value, return x if path fails
  • d(path, last=True): Return the deepest found value before a miss
  • d([paths...]): Resolve multiple paths → returns an ordered list (same order as input; missing paths yield None or default)
  • d[path]: Shorthand for d(path)
  • path in d: Check if path exists (True even if value is None)
  • d.scope(path): Create a new Dig rooted at that sub-path

Raises (on lookup)

  • TypeError: If path is not a str, tuple, or list; or if a multi-path list entry is not a str or tuple.
  • KeyError: If path passed to scope() does not exist.
  • TypeError: If the value at path passed to scope() is not a dict.

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 yield None (or default).
  • dig - a new scoped instance when calling scope().

Examples

Setup: wrap your nested dictionary
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
Basic lookups with dot-paths
# Navigate nested dicts with dot notation
d("user.name")
# 'Aditya Prasad S'

d("user.address.city")
# 'Kanyakumari'
Access lists by index
# Numeric segments in dot-paths become list indices
d("user.projects.0.name")
# 'funcBox'

d("user.projects.1.lang")
# 'Kotlin'
Handle missing keys gracefully
# 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'}
Use tuple paths for explicit keys
# Useful when keys contain dots, or to avoid string parsing
d(("user", "projects", 0, "stars"))
# 42
Subscript shorthand and membership tests
# 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)
Query multiple paths at once
# 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']
Scope into sub-sections (avoid repeating paths)
# 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']
Scope into list elements
# Scoping works with list indices too
proj = d.scope("user.projects.0")

proj("name")
# 'funcBox'

proj("stars")
# 42
Nested scopes
# 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
String representation
# 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.


Disclaimer

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.

Contributing

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.

Issues & Bug Reports

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.

License

Licensed under the MIT License. See LICENSE for details.

Copyright © 2026

About

FuncBox is a streamlined Python utility library designed to provide essential utilities.

Topics

Resources

License

Stars

Watchers

Forks

Contributors

Languages