Skip to content

qs-codec

qs-codec

A query string encoding and decoding library for Python.

Ported from qs for JavaScript.

PyPI versionPyPI downloadsPyPI statusPython version supportPyPy support statusPyPI formatTest StatusCodeQL StatusPublish StatusDocs StatusCode CoverageCodacy QualityOpenSSF Best PracticesCode style Blackflake8 Statustyping mypylinting pylintimports isortSecurity StatusLicenseContributor CovenantGitHub SponsorsGitHub Repo stars

Highlights

  • 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=c as 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/decoder callables; options to sort keys, filter output, and control percent-encoding (keys-only, values-only).
  • Nulls & empties: strict_null_handling and skip_nulls; support for empty lists/arrays when desired.
  • Dates: serialize_date for 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.

Compatibility

  • CPython 3.8–3.14 (default tox envs).
  • PyPy 3.8–3.11 (run tox -e pypy3.8 through tox -e pypy3.11 locally; CI mirrors this matrix).

Usage

A simple usage example:

importqs_codecasqs# Encodingassertqs.encode({'a': 'b'}) =='a=b'# Decodingassertqs.decode('a=b') == {'a': 'b'}

Compared with urllib.parse

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.

Working with URLs

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.

Decoding

dictionaries

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.

lists

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}'``)

primitive values (int, bool, None, etc.)

By default, all values are parsed as strings.

importqs_codecasqsassertqs.decode(
'a=15&b=true&c=null',
) == {'a': '15', 'b': 'true', 'c': 'null'}

Encoding

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'

Maximum encoding depth

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'

Handling None values

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'

Dealing with special character sets

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': 'こんにちは!'}

RFC 3986 and RFC 1738 space encoding

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'

Other ports

PortRepositoryPackage
Darttechouse/qspub.dev version
Kotlin / JVM + Android AARtechouse/qs-kotlinMaven Central version
Swift / Objective-Ctechouse/qs-swiftSwift Package Manager version
.NET / C#techouse/qs-netNuGet version
Rusttechouse/qs_rustcrates.io version
Node.js (original)ljharb/qsnpm version

Special thanks to the authors of qs for JavaScript: - Jordan Harband - TJ Holowaychuk

About

A query string encoding and decoding library for Python. Ported from qs for JavaScript.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

14 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages