jmespath
jmespath is the Python implementation of JMESPath, a query language for JSON: you write a declarative expression like 'reservations[*].instances[?state==`running`].id' and it extracts exactly that from nested dicts and lists. The language has a formal ABNF grammar, a spec, and a cross-implementation compliance suite, so expressions behave identically in Python, the AWS CLI, and other ports. The 150M weekly downloads are mostly because boto3 and the AWS CLI depend on it, so it is already installed on practically every machine that touches AWS.
The most disciplined JSON query language in Python: spec-frozen, portable, and already installed anywhere boto3 lives. Use it for config-driven extraction and anything AWS-adjacent; skip it when you need recursive search or real transformation, which are exactly the features it refuses to have.
Use it if
- You need user-supplied or config-driven JSON queries: a JMESPath string in YAML config is far safer and more portable than eval or ad-hoc key paths
- You work with AWS: boto3 paginators, the CLI's --query flag, and CloudWatch tooling all speak JMESPath, so learning it once pays off across the stack
- You want spec-frozen behavior: the language has a formal grammar and compliance tests, so an expression that works today works in every version and implementation
- You are already shipping boto3, which means jmespath is in your environment for free
- You need recursive descent: JMESPath has no equivalent of JSONPath's '..' operator, so 'find this key at any depth' requires jsonpath-ng instead
- You need to transform or mutate data, not just extract it: reshaping beyond multiselect hashes gets unreadable fast, and glom or plain Python is clearer
- You control the data shape in code: a two-line dict comprehension beats introducing a query language your teammates have to learn
- Performance-critical hot loops over huge documents: it is pure Python tree-walking, and a hand-written extraction is faster
Setup reality
pip install jmespath is as easy as it gets: pure Python, zero dependencies, supports Python 3.9+. The friction is in the language, not the install: missing keys return None silently instead of raising, so typos in expressions fail quietly; numeric and string literals need backticks or single quotes with rules people always get wrong; and the custom-functions API has been labeled experimental for a decade. Also know the release rhythm: 1.1.0 in January 2026 was the first release since 1.0.1 in 2022, which is fine for a spec-frozen language but means bugs sit for years.
Patterns
Extract a nested valuebasic-search
import jmespath
data = {"foo": {"bar": "baz"}}
jmespath.search("foo.bar", data) # 'baz'
jmespath.search("foo.missing", data) # None, no exceptionMissing paths return None silently rather than raising. Great for optional fields, terrible for catching typos in expressions; test expressions against real payloads.
Compile once, search many documentscompile-reuse
import jmespath
expr = jmespath.compile("items[*].id")
for page in pages:
ids = expr.search(page)compile() parses the expression once; in loops over many documents this skips reparsing, the same way re.compile does for regexes.
Project over listslist-projection
data = {"users": [{"name": "ana"}, {"name": "raj"}]}
jmespath.search("users[*].name", data) # ['ana', 'raj']
jmespath.search("users[-1].name", data) # 'raj'
jmespath.search("users[0:1].name", data) # ['ana'] (python-style slice)Projections stop at None: if 'users' is missing the whole expression yields None, not an empty list. Guard with '|| `[]`' when downstream code expects a list.
Filter with a predicatefilter-expression
data = {"instances": [
{"id": "i-1", "state": "running", "cpu": 4},
{"id": "i-2", "state": "stopped", "cpu": 8},
]}
jmespath.search("instances[?state=='running'].id", data) # ['i-1']
jmespath.search("instances[?cpu > `4`].id", data) # ['i-2']String literals use single quotes, numbers need backticks: 'cpu > 4' without backticks is a parse error people hit constantly.
Reshape with multiselect hashesmultiselect-reshape
data = {"instances": [{"id": "i-1", "state": "running", "az": "us-east-1a"}]}
jmespath.search(
"instances[*].{name: id, zone: az}", data
) # [{'name': 'i-1', 'zone': 'us-east-1a'}]Multiselect ({} for dicts, [] for lists) is the only reshaping JMESPath does; anything more complicated is a sign you want glom or plain Python.
Pipe results through built-in functionspipe-and-functions
data = {"pkgs": [{"name": "a", "dl": 90}, {"name": "b", "dl": 300}]}
jmespath.search("sort_by(pkgs, &dl)[-1].name", data) # 'b'
jmespath.search("pkgs[*].dl | max(@)", data) # 300
jmespath.search("length(pkgs)", data) # 2'&' makes an expression-reference (needed by sort_by, max_by, min_by) and '@' is the current node. The built-in function list is fixed by the spec.
Flatten nested listsflatten-nested
data = {"reservations": [
{"instances": [{"id": "i-1"}, {"id": "i-2"}]},
{"instances": [{"id": "i-3"}]},
]}
jmespath.search("reservations[*].instances[*].id", data)
# [['i-1', 'i-2'], ['i-3']]
jmespath.search("reservations[].instances[].id", data)
# ['i-1', 'i-2', 'i-3'][*] preserves nesting per projection level, [] flattens one level. This exact pair (reservations/instances) is the canonical AWS EC2 example because everyone hits it there.
Filter boto3 pagination with JMESPathboto3-paginate-search
import boto3
ec2 = boto3.client("ec2")
paginator = ec2.get_paginator("describe_instances")
running = paginator.paginate().search(
"Reservations[].Instances[?State.Name=='running'].InstanceId[]"
)
for instance_id in running:
print(instance_id)PageIterator.search() applies the expression across all pages lazily; this is jmespath's biggest real-world deployment and mirrors the AWS CLI --query flag exactly.
Register a custom functioncustom-functions
import jmespath
from jmespath import functions
class Custom(functions.Functions):
@functions.signature({"types": ["string"]})
def _func_upper(self, s):
return s.upper()
opts = jmespath.Options(custom_functions=Custom())
jmespath.search("upper(name)", {"name": "keyoor"}, options=opts) # 'KEYOOR'Method names must start with _func_ and the signature decorator enforces argument types. The README has marked this API experimental for years, so pin if you rely on it.
Provide fallbacks with the or-operatordefault-fallback
data = {"config": {}}
jmespath.search("config.timeout || `30`", data) # 30
jmespath.search("config.tags || `[]`", data) # []'||' returns the right side when the left is null or empty, which is the idiomatic way to avoid None leaking into code that expects a value.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| jsonpath-ng | PyPI | You need JSONPath semantics, especially recursive descent ('$..price') that JMESPath deliberately does not have. |
| glom | PyPI | You want Pythonic path access with defaults, deep assignment, and data reshaping rather than a separate query-string language. |
| jq | PyPI | Your team already thinks in jq syntax and you want the same expressions in Python via bindings to the C library. |