| name | qs-codec |
| description | Use this skill whenever a user wants to install, configure, troubleshoot, or write Python application code for encoding and decoding nested query strings with the qs-codec package, including comparing or composing qs-codec with urllib.parse.urlencode, parse_qs, parse_qsl, and URL components. This skill helps produce practical qs_codec.decode, qs_codec.encode, qs_codec.loads, and qs_codec.dumps snippets, choose DecodeOptions and EncodeOptions, explain option tradeoffs, and avoid qs-codec edge-case pitfalls around lists, dot notation, duplicates, null handling, charset sentinels, depth limits, URL replacement, and untrusted input. |
qs-codec Usage Assistant
Help users parse and build query strings with the Python qs-codec package.
Focus on user application code and interoperability outcomes, not repository
maintenance.
Start With Inputs
Before producing a final snippet, collect only the missing details that change
the code:
- Runtime: Python script, web framework, tests, library code, or generated
example.
- Direction: decode an incoming query string, encode Python data, or normalize
query-string handling around an existing URL/request object.
- The actual query string or Python structure when available.
- Target API convention for lists: indexed brackets, empty brackets, repeated
keys, or comma-separated values.
- Whether the query may include a leading
?, dot notation, literal dots in
keys, duplicate keys, custom delimiters, comma-separated lists, None flags,
Latin-1/legacy charset behavior, or untrusted user input.
Do not over-ask when the desired behavior is obvious. State assumptions in the
answer and give the user a concrete snippet they can paste.
Installation
Install the package from PyPI:
python -m pip install qs-codec
Use the package-level public API:
import qs_codec as qs
When snippets use regex delimiters, dates, or custom codecs, include the needed
standard-library imports such as re, datetime, codecs, or typing.
Base Patterns
Decode a query string into nested Python values:
import qs_codec as qs
params = qs.decode("a[b][c]=d&tags[]=python&tags[]=web")
assert params == {"a": {"b": {"c": "d"}}, "tags": ["python", "web"]}
Encode nested Python values into a query string:
import qs_codec as qs
query = qs.encode({
"a": {"b": {"c": "d"}},
"tags": ["python", "web"],
})
assert query == "a%5Bb%5D%5Bc%5D=d&tags%5B0%5D=python&tags%5B1%5D=web"
Use qs.loads(...) as a string-only alias for qs.decode(...), and
qs.dumps(...) as an alias for qs.encode(...).
Standard-Library Codec Selection
Choose urllib.parse.urlencode, parse_qs, and parse_qsl for conventional
flat application/x-www-form-urlencoded data. Choose qs-codec when callers
need nested dictionaries or lists, Node qs interoperability, configurable
list, duplicate, or null semantics, or matching structure-aware encode/decode
behavior.
Apply these distinctions when comparing the APIs:
- Treat
urlencode as a flat encoder. With doseq=True, expand sequence
values as repeated keys; use ListFormat.REPEAT for the equivalent
qs.encode output. Do not pass nested mappings to urlencode expecting
recursive query paths.
- Account for different defaults:
urlencode emits spaces as + and uses
Python scalar spellings such as True and None; qs.encode defaults to
%20, lowercase booleans, and an empty value for None.
- Treat
parse_qs as a grouped flat parser whose dictionary values are always
lists. It leaves bracket syntax in literal keys, drops blanks unless
keep_blank_values=True, and cannot distinguish a name-only token from an
explicit empty value.
- Prefer
parse_qsl when flat pair order and interleaved duplicates matter. It
returns an ordered list of name/value pairs, but otherwise shares
parse_qs's literal bracket, blank-value, and null-distinction limitations.
It also percent-decodes names and values and normalizes + and %20, so do
not present its output as a lossless representation of the raw query.
- Use
qs.decode to reconstruct bracket or dot paths. A singleton normally
remains scalar, duplicate policies support combine/first/last, and
strict_null_handling=True distinguishes a bare name as None from an
explicit empty string.
- Note that all three parsers leave primitive values as strings.
parse_qs and
qs.decode combine repeated flat keys by default, while parse_qsl retains
each occurrence. For resource limits, compare the standard-library parsers'
max_num_fields with DecodeOptions.parameter_limit, depth, and
list_limit plus raise_on_limit_exceeded.
Do not preprocess a complete URL's raw query with parse_qs or parse_qsl
before qs.decode; doing so flattens structured syntax and loses name-only
distinctions.
Standard-Library URL Recipes
Use urllib.parse.urlsplit to separate URL parsing from qs decoding. Pass the
encoded .query component directly to qs.decode; do not call unquote,
unquote_plus, parse_qs, or parse_qsl first. Pre-decoding can turn escaped
delimiters into structure, double-decode percent signs, and flatten qs bracket
syntax.
from urllib.parse import urlsplit
import qs_codec as qs
parts = urlsplit(
"https://example.com/search?filter%5Bname%5D=Jane%20Doe&flag#results"
)
params = qs.decode(
parts.query,
qs.DecodeOptions(strict_null_handling=True),
)
assert params == {"filter": {"name": "Jane Doe"}, "flag": None}
For a bytes URL, urlsplit returns a byte query while qs.decode accepts
text. Use .query.decode("ascii") only when the application boundary guarantees
a conforming ASCII percent-encoded URL; otherwise ask the caller to define the
outer byte-decoding policy.
Replace a query with freshly encoded data:
updated = parts._replace(
query=qs.encode({
"filter": {"name": "John Doe"},
"tags": ["a", "b"],
}),
).geturl()
Apply these constraints when recommending URL composition:
- Keep
EncodeOptions.add_query_prefix=False when assigning to
SplitResult.query; a prefixed encoded value creates ??.
- Default percent-encoded output is appropriate for replacement. Treat
encode=False, encode_values_only=True, custom encoders, and raw query text
as caller-managed because they can emit #, &, ?, or malformed percent
escapes.
- Describe
_replace(query=...).geturl() as replacement, not append or merge.
It intentionally discards the existing query and may normalize URL spelling;
empty encoded output removes an explicit query delimiter.
- Do not propose a generic append helper when existing and new queries may use
different delimiters. Mixing
& and ; cannot be interpreted generally
without choosing a parser and rewriting one side.
- Do not decode and re-encode an arbitrary existing query to "normalize" it.
That can regroup interleaved duplicates, convert bare names to empty values,
change list formats and delimiters, reorder tokens, and select new percent
spellings.
- Use the direct standard-library expression for raw replacement. A wrapper
around
_replace(query=raw).geturl() does not add validation or escaping.
Decode Recipes
Use these options with qs.decode(query, qs.DecodeOptions(...)):
- Leading question mark:
ignore_query_prefix=True.
- Dot notation such as
a.b=c: allow_dots=True.
- Double-encoded literal dots in keys such as
name%252Eobj.first=John:
decode_dot_in_keys=True.
- Duplicate keys:
duplicates=qs.Duplicates.COMBINE keeps all values as a
list; use qs.Duplicates.FIRST or qs.Duplicates.LAST to collapse.
- Bracket lists: enabled by default; set
parse_lists=False to treat list
syntax as dictionary keys.
- List limits: default
list_limit is 20; numeric indices at or above the
limit become dictionary keys. The limit also applies cumulatively to lists
grown by duplicate keys, mixed notation, or comma-separated values. Exact-limit
results remain lists; soft overflow becomes a numeric-keyed dictionary, while
raise_on_limit_exceeded=True raises ValueError.
- Comma-separated values such as
a=b,c: comma=True.
- Tokens without
= as None: strict_null_handling=True.
- Custom delimiters:
delimiter=";" or delimiter=re.compile(r"[;,]").
- Legacy charset input:
charset=qs.Charset.LATIN1; use
charset_sentinel=True when a form may include utf8=... to signal the real
charset.
- HTML numeric entities:
interpret_numeric_entities=True, usually with
Latin-1 or charset sentinel handling.
- Untrusted input: keep
depth, parameter_limit, and list_limit bounded;
use strict_depth=True plus raise_on_limit_exceeded=True when callers need
hard failures instead of soft limiting.
Example for a request query:
import qs_codec as qs
params = qs.decode(
"?filter.status=open&tag=python&tag=web",
qs.DecodeOptions(
ignore_query_prefix=True,
allow_dots=True,
duplicates=qs.Duplicates.COMBINE,
),
)
assert params == {"filter": {"status": "open"}, "tag": ["python", "web"]}
Encode Recipes
Use these options with qs.encode(data, qs.EncodeOptions(...)):
- List style defaults to
qs.ListFormat.INDICES:
tags%5B0%5D=python&tags%5B1%5D=web.
- Empty brackets:
list_format=qs.ListFormat.BRACKETS.
- Repeated keys:
list_format=qs.ListFormat.REPEAT.
- Comma-separated values:
list_format=qs.ListFormat.COMMA.
- Single-item comma lists that must round-trip as lists:
comma_round_trip=True.
- Drop
None items before comma-joining lists: comma_compact_nulls=True.
- Dot notation for nested dictionaries:
allow_dots=True.
- Literal dots in keys:
encode_dot_in_keys=True; leave allow_dots
unspecified or set it explicitly based on whether nested paths should use
dot notation.
- Add a leading
?: add_query_prefix=True.
- Custom pair delimiter:
delimiter=";".
- Preserve readable bracket/dot keys while encoding values:
encode_values_only=True.
- Disable percent encoding entirely for debugging or documented examples:
encode=False.
- Emit
None without =: strict_null_handling=True.
- Omit
None keys: skip_nulls=True.
- Emit empty lists as
foo[]: allow_empty_lists=True.
- Omit arbitrary keys by filtering them out of the input mapping before
encoding; avoid internal sentinels in application snippets.
- Legacy form spaces as
+: format=qs.Format.RFC1738; the default is
qs.Format.RFC3986, which emits spaces as %20.
- Legacy charset output:
charset=qs.Charset.LATIN1; use
charset_sentinel=True to prepend the utf8=... sentinel.
- Custom behavior: use
encoder, serialize_date, sort, or filter when
the target API needs special scalar encoding, date formatting, stable key
order, or selected fields.
- Maximum traversal depth:
max_depth=<positive int>; None means unbounded by
this option.
Example for an API that expects repeated keys:
import qs_codec as qs
query = qs.encode(
{
"q": "query strings",
"tag": ["python", "web"],
},
qs.EncodeOptions(
list_format=qs.ListFormat.REPEAT,
add_query_prefix=True,
),
)
assert query == "?q=query%20strings&tag=python&tag=web"
Combinations To Check
Warn or adjust before giving code for these cases:
qs.DecodeOptions(decode_dot_in_keys=True, allow_dots=False) is invalid.
parameter_limit must be positive or float("inf"); use
raise_on_limit_exceeded=True to raise when the limit is exceeded instead of
silently truncating.
list_limit has nuanced list-construction behavior; negative values disable
numeric-index list parsing, and raise_on_limit_exceeded=True turns list
limit violations into ValueError. With comma=True, a flat comma value is
checked before value decoding, while a comma group assigned through []=
counts as one outer list element.
- Built-in charset handling supports only
qs.Charset.UTF8 and
qs.Charset.LATIN1; other encodings require a custom encoder or decoder.
EncodeOptions.encoder is ignored when encode=False.
- Combining
encode_values_only=True and encode_dot_in_keys=True encodes only
dots in keys; values remain otherwise unchanged.
DecodeOptions.comma parses simple comma-separated values, but does not
decode nested dictionary syntax such as a={b:1},{c:d}.
encode(None), scalar roots, empty dictionaries, and empty containers
generally produce an empty string.
- The standard library and many web frameworks flatten duplicates or nested
query syntax. Prefer
qs.decode on the raw query string when qs-style nested
or repeated values matter.
Response Shape
For code-generation requests, answer with:
- A short statement of assumptions, especially list format, null handling,
charset, prefix handling, and whether input is trusted.
- One concrete Python snippet using
qs.decode, qs.encode, qs.loads, or
qs.dumps.
- A brief explanation of only the options used.
- A small verification example, such as an expected dictionary, expected query
string, or a
pytest assertion.
Keep snippets application-oriented. Prefer public API imports from qs_codec;
do not ask users to import from qs_codec.src or private modules.