What is punycode?

Punycode, defined
Punycode encodes a Unicode domain name as ASCII with an xn-- prefix, so the existing DNS system can carry names written in scripts it was never designed for.

The encoding is reversible and deterministic. münchen.de becomes xn--mnchen-3ya.de, and any resolver can convert in either direction without a lookup.

DNS only ever sees the ASCII form. Your application converts before querying, which is why a validation library that lacks an IDNA implementation cannot check these domains at all.

Browsers decide what to display based on script mixing. A domain combining Cyrillic and Latin characters is shown in its encoded form deliberately, as a warning that it may be imitating something.

For a verifier the conversion is a required first step. Skip it and the DNS query fails, producing an invalid verdict for a domain that is entirely real.

How ZapBounce reports it

Non-ASCII domains are converted to Punycode before the MX lookup, and the result echoes back the address you submitted rather than the encoded form. A domain that fails only because a library could not encode it is a bug, not an invalid address.

Converting a name and reading it back

Take bücher.example. Each dot-separated label is handled separately. example is already ASCII and stays as it is. For bücher, the encoder pulls out the ASCII letters bcher, then appends a hyphen and a short code, kva, that records which character was removed and where it belongs. Add the prefix and you get xn--bcher-kva.example.

You can try it without installing anything. In Python, 'bücher.example'.encode('idna') returns xn--bcher-kva.example, and decoding it brings the original back. Each encoded label still has to fit DNS's 63-character limit, prefix included, which long names in some scripts can hit sooner than you'd guess.

When you're debugging a lookup, remember that DNS tools want the xn-- form. If you keep logs, record both versions of the name. The readable one is what you'll recognize later, and the encoded one is what you'll need to paste into dig.

When two libraries disagree about the same name

There are two generations of the standard, and they don't treat every character the same way. The German sharp s is the textbook case. Under the 2003 rules, straße.example is mapped to strasse.example before lookup. The 2008 rules say is a valid character of its own, and the name encodes as xn--strae-oqa.example. Those are two different domains that can have two different owners.

So the language or library you use decides which server your mail goes to. Python's built-in codec follows the older rules, while most current browsers follow the newer ones. If your signup form runs one and your sending service runs the other, a small number of addresses will resolve differently in each place.

Lookalike names are the other hazard. In 2017 a researcher registered xn--80ak6aa92e.com, which is made entirely of Cyrillic letters and displayed in several browsers as a well-known brand's name. For mail, a sensible rule is to show the xn-- form to your staff whenever a domain mixes scripts or arrives already encoded, so a person gets a chance to notice.

Punycode: common questions

Why do some domains start with xn--?

That prefix marks a Punycode-encoded internationalized name. The readable version uses characters DNS cannot carry directly.

Is Punycode reversible?

Yes, deterministically and without any lookup. Converting either way is pure computation.

Why does my browser show the encoded form?

As a safety measure when the name mixes scripts, since that pattern is how lookalike domains are built.

See this on your own list

100 free checks a month, and the unknowns come back labeled.