Python stringprep: Prepare Unicode

Published on: August 12, 2026
Reading time: 5 minutes
Secure Internet protocol representing Unicode preparation with Python stringprep

The stringprep module exposes the tables defined by RFC 3454 for preparing Unicode strings used in Internet protocols. Preparation can map or remove selected characters, normalize text, reject prohibited categories, and apply bidirectional rules before identifiers are compared, stored, or transmitted.

The module does not provide one universal cleaning function. It exposes table lookup and mapping functions. A protocol profile must define which tables apply, whether case folding is required, which normalization form to use, and how to handle unassigned characters.

A historical standards module

RFC 3454 supported profiles such as Nameprep in early internationalized domain-name systems. Its tables are based on Unicode 3.2, so recently added characters may appear unassigned in this historical model.

Do not invent a profile for authentication, domain names, or security-sensitive identifiers. Use the current standard and a dedicated implementation when available. stringprep is most useful for legacy interoperability, audits, and understanding protocol preparation.

Tables exposed as functions

The RFC tables are too large to expose as ordinary dictionaries, so Python provides characteristic functions for sets and mapping functions for transformations.

import stringprep

character = "\u00ad"  # soft hyphen
print(stringprep.in_table_b1(character))

Table B.1 includes characters commonly mapped to nothing. The lookup does not modify text; the application performs the transformation.

Unassigned code points

in_table_a1() reports whether a code point was unassigned in Unicode 3.2.

def has_unassigned(text):
    return any(stringprep.in_table_a1(c) for c in text)

A profile may reject these points so future assignments cannot change identifier meaning. However, modern characters may also be marked unassigned by this legacy table.

Map characters to nothing

def apply_b1(text):
    return "".join(
        c for c in text
        if not stringprep.in_table_b1(c)
    )

Removing characters can make two different inputs converge. Detect collisions after the complete preparation and preserve the original value for display and auditing.

Case folding with B.2 and B.3

map_table_b2() provides case folding intended for use with NFKC. map_table_b3() provides a mapping used without normalization.

def map_b2(text):
    return "".join(stringprep.map_table_b2(c) for c in text)

A mapping result may contain multiple characters. Do not assume one input character produces one output character or validate length before transformation.

NFKC normalization

Many historical profiles apply NFKC after mapping.

import unicodedata

def prepare_base(text):
    text = apply_b1(text)
    text = map_b2(text)
    return unicodedata.normalize("NFKC", text)

NFKC removes compatibility distinctions and may change width variants, stylistic characters, and symbols. Apply it only when the protocol requires it and retain original display text.

ASCII and non-ASCII spaces

Tables C.1.1 and C.1.2 identify ASCII and non-ASCII spaces. in_table_c11_c12() checks their union.

special_spaces = [
    c for c in text
    if stringprep.in_table_c11_c12(c)
]

A profile may map, prohibit, or selectively allow these characters. Do not replace every space automatically without the specification.

Control characters

Tables C.2.1 and C.2.2 cover ASCII and non-ASCII control characters.

def has_control(text):
    return any(
        stringprep.in_table_c21_c22(c)
        for c in text
    )

Controls can manipulate logs, terminals, and protocol framing. Escape invisible characters in diagnostics even if a protocol permits some of them.

Private use, noncharacters, and surrogates

in_table_c3(), in_table_c4(), and in_table_c5() identify private-use points, noncharacters, and surrogate codes.

Reject them according to the profile and before serialization into systems that require valid Unicode scalar values.

Other prohibited categories

C.6 and C.7 identify characters inappropriate for plain text or canonical representation. C.8 covers characters that change display properties or are deprecated. C.9 covers tagging characters.

These categories demonstrate why strip() plus lower() is not protocol preparation.

Bidirectional rules

D.1 identifies characters with R or AL bidi properties. D.2 identifies L characters. RFC 3454 profiles usually impose special rules when right-to-left characters occur.

def validate_bidi(text):
    has_randal = any(stringprep.in_table_d1(c) for c in text)
    if not has_randal:
        return True
    if any(stringprep.in_table_d2(c) for c in text):
        return False
    return (
        stringprep.in_table_d1(text[0])
        and stringprep.in_table_d1(text[-1])
    )

This demonstrates the historical basic rule, but production code must follow the exact profile. Logical and visual order can differ, so security tools should display escaped representations.

A profile-oriented example

import unicodedata

class StringPrepError(ValueError):
    pass

def prepare_legacy(text):
    mapped = "".join(
        "" if stringprep.in_table_b1(c)
        else stringprep.map_table_b2(c)
        for c in text
    )
    normalized = unicodedata.normalize("NFKC", mapped)

    prohibited = (
        stringprep.in_table_c12,
        stringprep.in_table_c21_c22,
        stringprep.in_table_c3,
        stringprep.in_table_c4,
        stringprep.in_table_c5,
        stringprep.in_table_c6,
        stringprep.in_table_c7,
        stringprep.in_table_c8,
        stringprep.in_table_c9,
    )
    for c in normalized:
        if any(table(c) for table in prohibited):
            raise StringPrepError(
                f"prohibited character U+{ord(c):04X}"
            )

    if not validate_bidi(normalized):
        raise StringPrepError("invalid bidi rule")
    return normalized

This is educational, not a complete named profile. The table set, A.1 policy, and exact order must come from the relevant standard.

Validate after transformation

Check character and byte limits after mapping and normalization because output length can change. If the prepared string becomes a unique database key, enforce uniqueness transactionally on the final value.

IDNA and domain names

Do not manually rebuild Nameprep for modern domain processing. Use an IDNA implementation matching the required standard. Domain handling includes label processing, Punycode, validation, and version-specific rules.

Passwords and usernames

Do not apply generic stringprep rules to passwords without a protocol specification. Mapping or removing characters can change the secret and reduce entropy. Modern protocols may use PRECIS profiles or other explicit rules.

For usernames, keep the original form and a separate prepared key. Monitor collisions and visually confusable identifiers.

Homographs

Case folding and NFKC do not merge every visually similar character. Latin, Greek, and Cyrillic characters may remain distinct. Sensitive public identifiers need script policies and specialized confusable detection.

Testing

Test control characters, non-ASCII spaces, B.1 removals, case-fold expansion, RTL text, mixed L/R text, empty input, and modern characters. Compare behavior with official protocol test vectors.

Version the preparation algorithm. Changing it for existing identifiers requires a migration and collision analysis.

Common mistakes

  • Assuming the module provides a complete sanitizer.
  • Creating a profile without a protocol standard.
  • Ignoring the Unicode 3.2 basis.
  • Checking size before mapping.
  • Failing to detect collisions.
  • Applying NFKC or removal rules to passwords arbitrarily.
  • Ignoring bidi rules and homographs.
  • Use a protocol-specific library.
  • Preserve original input.
  • Version the preparation function.
  • Follow the specified operation order.
  • Validate length and uniqueness at the end.
  • Escape invisible characters in logs.
  • Test official vectors and adversarial Unicode.

Continue with Python unicodedata, Python locale, Python textwrap, Python fnmatch, and Python contextvars.

See the official stringprep documentation and RFC 3454.

Conclusion

stringprep is a low-level toolbox for historical Unicode preparation profiles. It must be used under a defined specification, with awareness of Unicode 3.2, collisions, prohibited categories, and bidi rules. Prefer maintained protocol-specific implementations for modern systems.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Network connections representing non-blocking I/O with Python selectors
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python selectors: Non-Blocking I/O

    Learn Python selectors to monitor many sockets, read and write readiness, timeouts, partial messages, and non-blocking connections safely.

    Ler mais

    Tempo de leitura: 5 minutos
    11/08/2026
    Network data flow representing asynchronous context with Python contextvars
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python contextvars: Async Context State

    Learn Python contextvars to store task-local state, prevent asyncio leaks, copy contexts, propagate metadata, and restore values with tokens.

    Ler mais

    Tempo de leitura: 4 minutos
    11/08/2026
    Programming code representing operations as functions with Python operator
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python operator: Operations as Functions

    Learn Python operator to use operations as functions, sort fields, access items, call methods, and build clear functional pipelines.

    Ler mais

    Tempo de leitura: 4 minutos
    11/08/2026
    Three-dimensional alphabet representing Unicode normalization with Python unicodedata
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python unicodedata: Normalize Unicode

    Learn Python unicodedata to normalize Unicode, inspect names, categories, numeric values, combining marks, bidirectional classes, and width.

    Ler mais

    Tempo de leitura: 5 minutos
    11/08/2026
    Server network representing resource management with Python ExitStack
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python ExitStack: Manage Resources

    Learn Python ExitStack to manage dynamic files, connections, callbacks, and cleanup safely in predictable reverse order.

    Ler mais

    Tempo de leitura: 5 minutos
    11/08/2026
    Locked folder representing file types and permissions with Python stat
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python stat: File Types and Permissions

    Learn Python stat to interpret file types, permissions, links, timestamps, Windows attributes, and Unix flags safely and portably.

    Ler mais

    Tempo de leitura: 5 minutos
    10/08/2026