A query string encoding and decoding library for Python.
Ported from qs for JavaScript.
- Nested dictionaries & lists:
foo[bar][baz]=qux⇄{'foo': {'bar': {'baz': 'qux'}}}. - Multiple list formats: INDICES (
a[0]=x), BRACKETS (a[]=x), REPEAT (a=x&a=y), COMMA (a=x,y) with optional comma round-trip. - Dot-notation: parse/encode keys like
a.b=cas nested; option to encode dots in keys when using dot notation. - Charset handling: UTF-8 (default) and Latin-1; optional charset sentinel (
utf8=✓) to auto-detect encoding. - Pluggable hooks: custom
encoder/decodercallables; options to sort keys, filter output, and control percent-encoding (keys-only, values-only). - Nulls & empties:
strict_null_handlingandskip_nulls; support for empty lists/arrays when desired. - Dates:
serialize_datefor ISO 8601 or custom (e.g., UNIX timestamp). - Safety limits: configurable decode depth and encode max depth, parameter limit, and list element limit; optional strict-depth errors; duplicate-key strategies (combine/first/last).
- Extras: numeric entity decoding (e.g.
☺→ ☺), alternate delimiters/regex, and query-prefix helpers.
- CPython 3.8–3.14 (default tox envs).
- PyPy 3.8–3.11 (run
tox -e pypy3.8throughtox -e pypy3.11locally; CI mirrors this matrix).
A simple usage example:
importqs_codecasqs# Encodingassertqs.encode({'a': 'b'}) =='a=b'# Decodingassertqs.decode('a=b') == {'a': 'b'}The standard library's
urlencode,
parse_qs,
and
parse_qsl
are designed for conventional flat application/x-www-form-urlencoded
data. Use qs_codec when the query represents nested dictionaries or lists,
must interoperate with Node qs, or needs configurable list, duplicate, null,
or resource-limit behavior.
urlencode can expand a flat sequence into repeated keys with
doseq=True, which corresponds to ListFormat.REPEAT. It does not
recursively encode nested mappings; qs.encode uses bracket or dot paths
instead:
fromurllib.parseimporturlencodeimportqs_codecasqsasserturlencode({'tags': ['a', 'b']}, doseq=True) =='tags=a&tags=b'assertqs.encode(
{'tags': ['a', 'b']},
qs.EncodeOptions(list_format=qs.ListFormat.REPEAT),
) =='tags=a&tags=b'assertqs.encode(
{'filter': {'name': 'Jane'}},
) =='filter%5Bname%5D=Jane'The encoding defaults also differ: urlencode emits spaces as + and
uses Python scalar spellings such as True and None; qs.encode uses
%20, lowercase booleans, and an empty value for None by default.
On decode, parse_qs returns a dictionary whose values are always lists,
while parse_qsl returns an ordered list of name/value pairs and preserves
interleaved duplicate keys. Both treat bracket expressions as literal key
names, drop blank values unless keep_blank_values=True, and collapse a
name-only token and an explicit empty value to the same empty string.
qs.decode normally returns a scalar for one value, reconstructs bracket
paths, and can preserve that null distinction:
fromurllib.parseimportparse_qs, parse_qslimportqs_codecasqsquery='a=1&b=2&a=3&filter%5Bname%5D=Jane&flag&empty='assertparse_qs(query, keep_blank_values=True) == {
'a': ['1', '3'],
'b': ['2'],
'filter[name]': ['Jane'],
'flag': [''],
'empty': [''],
}
assertparse_qsl(query, keep_blank_values=True) == [
('a', '1'),
('b', '2'),
('a', '3'),
('filter[name]', 'Jane'),
('flag', ''),
('empty', ''),
]
assertqs.decode(
query,
qs.DecodeOptions(strict_null_handling=True),
) == {
'a': ['1', '3'],
'b': '2',
'filter': {'name': 'Jane'},
'flag': None,
'empty': '',
}All three decoders leave primitive values as strings. parse_qs and
qs.decode combine repeated flat keys under their default behavior;
parse_qsl instead retains each pair in input order. The standard-library
parsers offer max_num_fields; qs.decode additionally provides default
parameter, nesting-depth, and list limits plus configurable duplicate handling.
Use parse_qsl when flat pair order or duplicate interleaving matters, but
not as a raw-query round-trip format: it percent-decodes names and values,
normalizes + and %20 to the same space, and cannot retain the distinction
between a name-only token and an explicit empty value.
Use urllib.parse.urlsplit
to keep URL parsing separate from query-string decoding. Pass the encoded
query component directly to qs.decode without calling unquote,
unquote_plus, parse_qs, or parse_qsl first:
fromurllib.parseimporturlsplitimportqs_codecasqsparts=urlsplit(
'https://example.com/search?filter%5Bname%5D=Jane%20Doe&flag#results'
)
params=qs.decode(
parts.query,
qs.DecodeOptions(strict_null_handling=True),
)
assertparams== {
'filter': {'name': 'Jane Doe'},
'flag': None,
}Passing the encoded component unchanged ensures that escaped delimiters such as
%26, escaped percent signs such as %2525, and encoded bracket syntax
reach qs.decode without being decoded twice.
To replace a URL query, encode fresh data and assign it to the split result's
query component:
updated=parts._replace(
query=qs.encode({
'filter': {'name': 'John Doe'},
'tags': ['a', 'b'],
}),
).geturl()
assertupdated== (
'https://example.com/search?''filter%5Bname%5D=John%20Doe&tags%5B0%5D=a&tags%5B1%5D=b''#results'
)Keep EncodeOptions.add_query_prefix
set to False (the default) when assigning to SplitResult.query. Options
such as encode=False, encode_values_only=True, or a custom encoder can
emit raw URL-structural characters, so callers using them must ensure the result
is safe query-component text.
This pattern replaces the existing query; it does not append or merge it.
Appending or decoding and re-encoding an arbitrary query can change delimiter,
duplicate-key, name-only, list-format, ordering, and percent-encoding semantics.
SplitResult.geturl() may also normalize URL spelling and removes an explicit
empty ? delimiter.
decode allows you to create nested dicts within your query
strings, by surrounding the name of sub-keys with square brackets
[]. For example, the string 'foo[bar]=baz' converts to:
importqs_codecasqsassertqs.decode('foo[bar]=baz') == {'foo': {'bar': 'baz'}}URI encoded strings work too:
importqs_codecasqsassertqs.decode('a%5Bb%5D=c') == {'a': {'b': 'c'}}You can also nest your dicts, like 'foo[bar][baz]=foobarbaz':
importqs_codecasqsassertqs.decode('foo[bar][baz]=foobarbaz') == {'foo': {'bar': {'baz': 'foobarbaz'}}}By default, when nesting dicts qs will only decode up to 5
children deep. This means if you attempt to decode a string like
'a[b][c][d][e][f][g][h][i]=j' your resulting dict will be:
importqs_codecasqsassertqs.decode("a[b][c][d][e][f][g][h][i]=j") == {
"a": {"b": {"c": {"d": {"e": {"f": {"[g][h][i]": "j"}}}}}}
}This depth can be overridden by setting the depth:
importqs_codecasqsassertqs.decode(
'a[b][c][d][e][f][g][h][i]=j',
qs.DecodeOptions(depth=1),
) == {'a': {'b': {'[c][d][e][f][g][h][i]': 'j'}}}You can configure decode to throw an error
when parsing nested input beyond this depth using strict_depth (defaults to False):
importqs_codecasqstry:
qs.decode(
'a[b][c][d][e][f][g][h][i]=j',
qs.DecodeOptions(depth=1, strict_depth=True),
)
exceptIndexErrorase:
assertstr(e) =='Input depth exceeded depth option of 1 and strict_depth is True'The depth limit helps mitigate abuse when decode is used to parse user
input, and it is recommended to keep it a reasonably small number. strict_depth
adds a layer of protection by throwing an IndexError when the limit is exceeded, allowing you to catch and handle such cases.
For similar reasons, by default decode will only parse up to 1000 parameters. This can be overridden by passing a parameter_limit option:
importqs_codecasqsassertqs.decode(
'a=b&c=d',
qs.DecodeOptions(parameter_limit=1),
) == {'a': 'b'}To bypass the leading question mark, use ignore_query_prefix:
importqs_codecasqsassertqs.decode(
'?a=b&c=d',
qs.DecodeOptions(ignore_query_prefix=True),
) == {'a': 'b', 'c': 'd'}An optional delimiter can also be passed:
importqs_codecasqsassertqs.decode(
'a=b;c=d',
qs.DecodeOptions(delimiter=';'),
) == {'a': 'b', 'c': 'd'}delimiter can be a regular expression too:
importqs_codecasqsimportreassertqs.decode(
'a=b;c=d',
qs.DecodeOptions(delimiter=re.compile(r'[;,]')),
) == {'a': 'b', 'c': 'd'}Option allow_dots can be used to enable dot notation:
importqs_codecasqsassertqs.decode(
'a.b=c',
qs.DecodeOptions(allow_dots=True),
) == {'a': {'b': 'c'}}Option decode_dot_in_keys can be used to decode dots in keys.
Note: it implies allow_dots, so
decode will error if you set decode_dot_in_keys
to True, and allow_dots to False.
importqs_codecasqsassertqs.decode(
'name%252Eobj.first=John&name%252Eobj.last=Doe',
qs.DecodeOptions(decode_dot_in_keys=True),
) == {'name.obj': {'first': 'John', 'last': 'Doe'}}Option allow_empty_lists can
be used to allowing empty list values in a dict
importqs_codecasqsassertqs.decode(
'foo[]&bar=baz',
qs.DecodeOptions(allow_empty_lists=True),
) == {'foo': [], 'bar': 'baz'}Option duplicates can be used to change the behavior when duplicate keys are encountered
importqs_codecasqsassertqs.decode('foo=bar&foo=baz') == {'foo': ['bar', 'baz']}
assertqs.decode(
'foo=bar&foo=baz',
qs.DecodeOptions(duplicates=qs.Duplicates.COMBINE),
) == {'foo': ['bar', 'baz']}
assertqs.decode(
'foo=bar&foo=baz',
qs.DecodeOptions(duplicates=qs.Duplicates.FIRST),
) == {'foo': 'bar'}
assertqs.decode(
'foo=bar&foo=baz',
qs.DecodeOptions(duplicates=qs.Duplicates.LAST),
) == {'foo': 'baz'}If you have to deal with legacy browsers or services, there’s also support for decoding percent-encoded octets as LATIN1:
importqs_codecasqsassertqs.decode(
'a=%A7',
qs.DecodeOptions(charset=qs.Charset.LATIN1),
) == {'a': '§'}Some services add an initial utf8=✓ value to forms so that old
Internet Explorer versions are more likely to submit the form as utf-8.
Additionally, the server can check the value against wrong encodings of
the checkmark character and detect that a query string or
application/x-www-form-urlencoded body was not sent as utf-8,
e.g. if the form had an accept-charset parameter or the containing
page had a different character set.
decode supports this mechanism via the
charset_sentinel option.
If specified, the utf8 parameter will be omitted from the returned
dict. It will be used to switch to LATIN1 or
UTF8 mode depending on how the checkmark is encoded.
Important: When you specify both the charset
option and the charset_sentinel option, the
charset will be overridden when the request contains a
utf8 parameter from which the actual charset can be deduced. In that
sense the charset will behave as the default charset
rather than the authoritative charset.
importqs_codecasqsassertqs.decode(
'utf8=%E2%9C%93&a=%C3%B8',
qs.DecodeOptions(
charset=qs.Charset.LATIN1,
charset_sentinel=True,
),
) == {'a': 'ø'}
assertqs.decode(
'utf8=%26%2310003%3B&a=%F8',
qs.DecodeOptions(
charset=qs.Charset.UTF8,
charset_sentinel=True,
),
) == {'a': 'ø'}If you want to decode the &#...; syntax to the actual character, you can specify the interpret_numeric_entities option as well:
importqs_codecasqsassertqs.decode(
'a=%26%239786%3B',
qs.DecodeOptions(
charset=qs.Charset.LATIN1,
interpret_numeric_entities=True,
),
) == {'a': '☺'}It also works when the charset has been detected in charset_sentinel mode.
decode can also decode lists using a similar [] notation:
importqs_codecasqsassertqs.decode('a[]=b&a[]=c') == {'a': ['b', 'c']}You may specify an index as well:
importqs_codecasqsassertqs.decode('a[1]=c&a[0]=b') == {'a': ['b', 'c']}Note that the only difference between an index in a list and a key
in a dict is that the value between the brackets must be a number to
create a list. When creating lists with specific indices,
decode will compact a sparse list to
only the existing values preserving their order:
importqs_codecasqsassertqs.decode('a[1]=b&a[15]=c') == {'a': ['b', 'c']}Note that an empty string is also a value, and will be preserved:
importqs_codecasqsassertqs.decode('a[]=&a[]=b') == {'a': ['', 'b']}
assertqs.decode('a[0]=b&a[1]=&a[2]=c') == {'a': ['b', '', 'c']}decode also limits each list to a
maximum element count of 20. Index 19 is the last index that can create
a default list; index 20 and higher are converted to a dict with
the index as the key. This prevents inputs such as a[999999999] from
creating massive sparse lists.
importqs_codecasqsassertqs.decode('a[100]=b') == {'a': {'100': 'b'}}This limit can be overridden by passing a list_limit option:
importqs_codecasqsassertqs.decode(
'a[1]=b',
qs.DecodeOptions(list_limit=0),
) == {'a': {'1': 'b'}}The same limit is enforced cumulatively when duplicate keys, mixed list
notation, or comma-separated values grow a list. A result exactly at the limit
remains a list. Above the limit, decoding uses a numeric-keyed dict by
default, or raises ValueError when raise_on_limit_exceeded=True.
importqs_codecasqsassertqs.decode(
'a=x&a=y',
qs.DecodeOptions(list_limit=1),
) == {'a': {'0': 'x', '1': 'y'}}With comma=True, a flat comma value is subject to the same limit. A value
assigned through []= counts as one outer list element, so its inner
comma-separated group may contain more values than list_limit.
To disable list parsing entirely, set parse_lists
to False.
importqs_codecasqsassertqs.decode(
'a[]=b',
qs.DecodeOptions(parse_lists=False),
) == {'a': {'0': 'b'}}If you mix notations, decode will merge the two items into a dict:
importqs_codecasqsassertqs.decode('a[0]=b&a[b]=c') == {'a': {'0': 'b', 'b': 'c'}}You can also create lists of dicts:
importqs_codecasqsassertqs.decode('a[][b]=c') == {'a': [{'b': 'c'}]}(decodecannot convert nested ``dict``s, such as ``'a={b:1},{c:d}'``)
By default, all values are parsed as strings.
importqs_codecasqsassertqs.decode(
'a=15&b=true&c=null',
) == {'a': '15', 'b': 'true', 'c': 'null'}When encoding, encode by default URI encodes output. dicts are
encoded as you would expect:
importqs_codecasqsassertqs.encode({'a': 'b'}) =='a=b'assertqs.encode({'a': {'b': 'c'}}) =='a%5Bb%5D=c'This encoding can be disabled by setting the encode
option to False:
importqs_codecasqsassertqs.encode(
{'a': {'b': 'c'}},
qs.EncodeOptions(encode=False),
) =='a[b]=c'Encoding can be disabled for keys by setting the
encode_values_only option to True:
importqs_codecasqsassertqs.encode(
{
'a': 'b',
'c': ['d', 'e=f'],
'f': [
['g'],
['h']
]
},
qs.EncodeOptions(encode_values_only=True)
) =='a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h'You can cap how deep the encoder will traverse by setting the max_depth option. If unset, traversal is unbounded by this option. When set, the provided limit is enforced directly.
importqs_codecasqstry:
qs.encode({'a': {'b': {'c': 'd'}}}, qs.EncodeOptions(max_depth=2))
exceptValueErrorase:
assertstr(e) =='Maximum encoding depth exceeded'This encoding can also be replaced by a custom Callable in the
encoder option:
importqs_codecasqsimporttypingastdefcustom_encoder(
value: str,
charset: t.Optional[qs.Charset],
format: t.Optional[qs.Format],
) ->str:
ifvalue=='č':
return'c'returnvalueassertqs.encode(
{'a': {'b': 'č'}},
qs.EncodeOptions(encoder=custom_encoder),
) =='a[b]=c'(Note: the encoder option does not apply if
encode is False).
Similar to encoder there is a decoder option for decode to override decoding of properties and values:
importqs_codecasqsimporttypingastdefcustom_decoder(
value: t.Any,
charset: t.Optional[qs.Charset],
) ->t.Union[int, str]:
try:
returnint(value)
exceptValueError:
returnvalueassertqs.decode(
'foo=123',
qs.DecodeOptions(decoder=custom_decoder),
) == {'foo': 123}Examples beyond this point will be shown as though the output is not URI encoded for clarity. Please note that the return values in these cases will be URI encoded during real usage.
When lists are encoded, they follow the
list_format option, which defaults to
INDICES:
importqs_codecasqsassertqs.encode(
{'a': ['b', 'c', 'd']},
qs.EncodeOptions(encode=False)
) =='a[0]=b&a[1]=c&a[2]=d'You may override this by setting the indices option to
False, or to be more explicit, the list_format
option to REPEAT:
importqs_codecasqsassertqs.encode(
{'a': ['b', 'c', 'd']},
qs.EncodeOptions(
encode=False,
indices=False,
),
) =='a=b&a=c&a=d'You may use the list_format option to specify the
format of the output list:
importqs_codecasqs# ListFormat.INDICESassertqs.encode(
{'a': ['b', 'c']},
qs.EncodeOptions(
encode=False,
list_format=qs.ListFormat.INDICES,
),
) =='a[0]=b&a[1]=c'# ListFormat.BRACKETSassertqs.encode(
{'a': ['b', 'c']},
qs.EncodeOptions(
encode=False,
list_format=qs.ListFormat.BRACKETS,
),
) =='a[]=b&a[]=c'# ListFormat.REPEATassertqs.encode(
{'a': ['b', 'c']},
qs.EncodeOptions(
encode=False,
list_format=qs.ListFormat.REPEAT,
),
) =='a=b&a=c'# ListFormat.COMMAassertqs.encode(
{'a': ['b', 'c']},
qs.EncodeOptions(
encode=False,
list_format=qs.ListFormat.COMMA,
),
) =='a=b,c'Note: When using list_format set to
COMMA, you can also pass the
comma_round_trip option set to True or
False, to append [] on single-item lists, so that they can round trip through a decoding.
Set the comma_compact_nulls option to True with the same
format when you'd like to drop None entries instead of keeping empty slots (e.g. [True, False, None, True] becomes
true,false,true).
BRACKETS notation is used for encoding dicts by default:
importqs_codecasqsassertqs.encode(
{'a': {'b': {'c': 'd', 'e': 'f'}}},
qs.EncodeOptions(encode=False),
) =='a[b][c]=d&a[b][e]=f'You may override this to use dot notation by setting the
allow_dots option to True:
importqs_codecasqsassertqs.encode(
{'a': {'b': {'c': 'd', 'e': 'f'}}},
qs.EncodeOptions(encode=False, allow_dots=True),
) =='a.b.c=d&a.b.e=f'You may encode dots in keys of dicts by setting
encode_dot_in_keys to True:
importqs_codecasqsassertqs.encode(
{'name.obj': {'first': 'John', 'last': 'Doe'}},
qs.EncodeOptions(
allow_dots=True,
encode_dot_in_keys=True,
),
) =='name%252Eobj.first=John&name%252Eobj.last=Doe'Caveat: When both encode_values_only
and encode_dot_in_keys are set to
True, only dots in keys and nothing else will be encoded!
You may allow empty list values by setting the
allow_empty_lists option to True:
importqs_codecasqsassertqs.encode(
{'foo': [], 'bar': 'baz', },
qs.EncodeOptions(
encode=False,
allow_empty_lists=True,
),
) =='foo[]&bar=baz'Empty strings and None values will be omitted, but the equals sign (=) remains in place:
importqs_codecasqsassertqs.encode({'a': ''}) =='a='Keys with no values (such as an empty dict or list) will return nothing:
importqs_codecasqsassertqs.encode({'a': []}) ==''assertqs.encode({'a': {}}) ==''assertqs.encode({'a': [{}]}) ==''assertqs.encode({'a': {'b': []}}) ==''assertqs.encode({'a': {'b': {}}}) ==''The query string may optionally be prepended with a question mark (?) by setting
add_query_prefix to True:
importqs_codecasqsassertqs.encode(
{'a': 'b', 'c': 'd'},
qs.EncodeOptions(add_query_prefix=True),
) =='?a=b&c=d'The delimiter may be overridden as well:
importqs_codecasqsassertqs.encode(
{'a': 'b', 'c': 'd', },
qs.EncodeOptions(delimiter=';')
) =='a=b;c=d'If you only want to override the serialization of datetime
objects, you can provide a Callable in the
serialize_date option:
importqs_codecasqsimportdatetimeimportsys# First case: encoding a datetime object to an ISO 8601 stringassert (
qs.encode(
{
"a": (
datetime.datetime.fromtimestamp(7, datetime.UTC)
ifsys.version_info.major==3andsys.version_info.minor>=11elsedatetime.datetime.utcfromtimestamp(7)
)
},
qs.EncodeOptions(encode=False),
)
=="a=1970-01-01T00:00:07+00:00"ifsys.version_info.major==3andsys.version_info.minor>=11else"a=1970-01-01T00:00:07"
)
# Second case: encoding a datetime object to a timestamp stringassert (
qs.encode(
{
"a": (
datetime.datetime.fromtimestamp(7, datetime.UTC)
ifsys.version_info.major==3andsys.version_info.minor>=11elsedatetime.datetime.utcfromtimestamp(7)
)
},
qs.EncodeOptions(encode=False, serialize_date=lambdadate: str(int(date.timestamp()))),
)
=="a=7"
)To affect the order of parameter keys, you can set a Callable in the
sort option:
importqs_codecasqsassertqs.encode(
{'a': 'c', 'z': 'y', 'b': 'f'},
qs.EncodeOptions(
encode=False,
sort=lambdaa, b: (a>b) - (a<b)
)
) =='a=c&b=f&z=y'Finally, you can use the filter option to restrict
which keys will be included in the encoded output. If you pass a Callable, it will be called for each key to obtain
the replacement value. Otherwise, if you pass a list, it will be used to select properties and list indices to
be encoded:
importqs_codecasqsimportdatetimeimportsys# First case: using a Callable as filterassert (
qs.encode(
{
"a": "b",
"c": "d",
"e": {
"f": (
datetime.datetime.fromtimestamp(123, datetime.UTC)
ifsys.version_info.major==3andsys.version_info.minor>=11elsedatetime.datetime.utcfromtimestamp(123)
),
"g": [2],
},
},
qs.EncodeOptions(
encode=False,
filter=lambdaprefix, value: {
"b": None,
"e[f]": int(value.timestamp()) ifisinstance(value, datetime.datetime) elsevalue,
"e[g][0]": value*2ifisinstance(value, int) elsevalue,
}.get(prefix, value),
),
)
=="a=b&c=d&e[f]=123&e[g][0]=4"
)
# Second case: using a list as filterassertqs.encode(
{'a': 'b', 'c': 'd', 'e': 'f'},
qs.EncodeOptions(
encode=False,
filter=['a', 'e']
)
) =='a=b&e=f'# Third case: using a list as filter with indicesassertqs.encode(
{
'a': ['b', 'c', 'd'],
'e': 'f',
},
qs.EncodeOptions(
encode=False,
filter=['a', 0, 2]
)
) =='a[0]=b&a[2]=d'By default, None values are treated like empty strings:
importqs_codecasqsassertqs.encode({'a': None, 'b': ''}) =='a=&b='To distinguish between None values and empty strs use the
strict_null_handling flag.
In the result string the None values have no = sign:
importqs_codecasqsassertqs.encode(
{'a': None, 'b': ''},
qs.EncodeOptions(strict_null_handling=True),
) =='a&b='To decode values without = back to None use the
strict_null_handling flag:
importqs_codecasqsassertqs.decode(
'a&b=',
qs.DecodeOptions(strict_null_handling=True),
) == {'a': None, 'b': ''}To completely skip rendering keys with None values, use the
skip_nulls flag:
importqs_codecasqsassertqs.encode(
{'a': 'b', 'c': None},
qs.EncodeOptions(skip_nulls=True),
) =='a=b'If you’re communicating with legacy systems, you can switch to LATIN1 using the charset option:
importqs_codecasqsassertqs.encode(
{'æ': 'æ'},
qs.EncodeOptions(charset=qs.Charset.LATIN1)
) =='%E6=%E6'Characters that don’t exist in LATIN1 will be converted to numeric entities, similar to what browsers do:
importqs_codecasqsassertqs.encode(
{'a': '☺'},
qs.EncodeOptions(charset=qs.Charset.LATIN1)
) =='a=%26%239786%3B'You can use the charset_sentinel
option to announce the character by including an utf8=✓ parameter with the proper
encoding of the checkmark, similar to what Ruby on Rails and others do when submitting forms.
importqs_codecasqsassertqs.encode(
{'a': '☺'},
qs.EncodeOptions(charset_sentinel=True)
) =='utf8=%E2%9C%93&a=%E2%98%BA'assertqs.encode(
{'a': 'æ'},
qs.EncodeOptions(charset=qs.Charset.LATIN1, charset_sentinel=True)
) =='utf8=%26%2310003%3B&a=%E6'By default, the encoding and decoding of characters is done in UTF8, and LATIN1 support is also built in via the charset and charset parameter, respectively.
If you wish to encode query strings to a different character set (i.e. Shift JIS)
importqs_codecasqsimportcodecsimporttypingastdefcustom_encoder(
string: str,
charset: t.Optional[qs.Charset],
format: t.Optional[qs.Format],
) ->str:
ifstring:
buf: bytes=codecs.encode(string, 'shift_jis')
result: t.List[str] = ['{:02x}'.format(b) forbinbuf]
return'%'+'%'.join(result)
return''assertqs.encode(
{'a': 'こんにちは!'},
qs.EncodeOptions(encoder=custom_encoder)
) =='%61=%82%b1%82%f1%82%c9%82%bf%82%cd%81%49'This also works for decoding of query strings:
importqs_codecasqsimportreimportcodecsimporttypingastdefcustom_decoder(
string: str,
charset: t.Optional[qs.Charset],
) ->t.Optional[str]:
ifstring:
result: t.List[int] = []
whilestring:
match: t.Optional[t.Match[str]] =re.search(r'%([0-9A-F]{2})', string, re.IGNORECASE)
ifmatch:
result.append(int(match.group(1), 16))
string=string[match.end():]
else:
breakbuf: bytes=bytes(result)
returncodecs.decode(buf, 'shift_jis')
returnNoneassertqs.decode(
'%61=%82%b1%82%f1%82%c9%82%bf%82%cd%81%49',
qs.DecodeOptions(decoder=custom_decoder)
) == {'a': 'こんにちは!'}The default format is
RFC3986 which encodes
' ' to %20 which is backward compatible. You can also set the
format to
RFC1738 which encodes ' ' to +.
importqs_codecasqsassertqs.encode({'a': 'b c'}) =='a=b%20c'assertqs.encode(
{'a': 'b c'},
qs.EncodeOptions(format=qs.Format.RFC3986)
) =='a=b%20c'assertqs.encode(
{'a': 'b c'},
qs.EncodeOptions(format=qs.Format.RFC1738)
) =='a=b+c'| Port | Repository | Package |
|---|---|---|
| Dart | techouse/qs | |
| Kotlin / JVM + Android AAR | techouse/qs-kotlin | |
| Swift / Objective-C | techouse/qs-swift | |
| .NET / C# | techouse/qs-net | |
| Rust | techouse/qs_rust | |
| Node.js (original) | ljharb/qs |
Special thanks to the authors of qs for JavaScript: - Jordan Harband - TJ Holowaychuk
