UUID text forms: Python and .NET disagree on three of seven

RFC 9562 §4 defines a single canonical spelling, but standard-library parsers accept more than that and they do not accept the same set. Measured across seven spellings of one value, Python 3.13.5 and .NET 10.0.400 agree on four and disagree on three — in both directions. Neither is a superset of the other.

What the measurement shows

One UUID, seven spellings, two standard libraries. Python via uuid.UUID(), .NET via Guid.TryParse. Every row below was executed rather than read off documentation.

                                                      Python   .NET
f81d4fae-7dec-11d0-a765-00a0c91e6bf6     canonical      OK      OK
F81D4FAE-7DEC-11D0-A765-00A0C91E6BF6     uppercase      OK      OK
f81d4fae7dec11d0a76500a0c91e6bf6         no hyphens     OK      OK
{f81d4fae-7dec-11d0-a765-00a0c91e6bf6}   braced         OK      OK
urn:uuid:f81d4fae-7dec-11d0-a765-00a...  URN           OK    FAIL
f81d4fae7dec-11d0-a765-00a0c91e6bf6      hyphens moved  OK    FAIL
(f81d4fae-7dec-11d0-a765-00a0c91e6bf6)   parenthesised FAIL     OK

Four agreements and three disagreements, going opposite ways. Python takes the URN form and the malformed-hyphen form that .NET refuses; .NET takes the parenthesised form that Python refuses. There is no direction in which one library is simply more permissive, which is what makes this awkward: you cannot pick the stricter one and be safe.

What the specification actually requires

Less than the behaviour above suggests. RFC 9562 §4 gives the canonical form as 32 hex digits in 8-4-4-4-12 groups, says generated output should be lowercase, and says software reading a UUID should accept either case. That is the extent of it.

Braces, parentheses and the urn:uuid: prefix are platform conventions rather than alternative canonical forms. Braces come from the Windows registry and the COM-era GUID text format. Parentheses are another .NET format specifier. The URN prefix comes from RFC 4122 registering a uuid URN namespace, which is why it turns up in RDF, JSON-LD and anything else that identifies things with URIs. Each parser accepted whichever conventions its platform grew up with, and nobody was wrong.

The one that should worry you

Of the three disagreements, two are inconvenient and one is a real hazard. Python accepting urn:uuid: and .NET accepting parentheses are both cases of a parser being generous about a wrapper. You find them the first time a value crosses the boundary, and you add a strip call.

Python accepting hyphens in the wrong positions is different, because nothing about the input was a legitimate convention anywhere:

uuid.UUID("f81d4fae7dec-11d0-a765-00a0c91e6bf6")
-> UUID("f81d4fae-7dec-11d0-a765-00a0c91e6bf6")      Python: accepted

Guid.TryParse("f81d4fae7dec-11d0-a765-00a0c91e6bf6") -> false

The groups there are 12-4-4-12, not 8-4-4-4-12. Python reaches that result by stripping the prefix, stripping braces, and then removing every hyphen regardless of where it sits — so the position of the separators carries no meaning at all in its parser. If Python is the service that validates client input, a string like that passes, gets stored, gets logged, and is rejected by the first stricter consumer downstream. The exception then appears somewhere that has no idea where the value came from.

The fix is cheap: when the value arrives from outside your system, check the shape with a regular expression before you parse, rather than treating successful parsing as validation. Both operations are called “is this a UUID” in conversation and they are not the same question.

Two of the four runtimes have no parser at all

Node and Go sidestep the disagreement by not participating. Node’s crypto module exports exactly one UUID-related function — we listed the exports and found randomUUID and nothing else. There is no parser and no validator. Go’s standard library has no UUID package at all; every implementation in use is third-party.

So across four common runtimes, two define acceptance in a standard library and disagree with each other, and two leave it to whatever regular expression the nearest developer wrote. A UUID crossing a four-service system can be validated four different ways, and expecting a shared definition to exist is the assumption worth checking first.

Storage: pick one spelling and convert once

The hyphens carry no information, so CHAR(32) stores the same identifier as CHAR(36) in four fewer bytes per row, and both parsers we measured read the unhyphenated form back correctly. Case is the same kind of choice: RFC 9562 asks generators for lowercase, Python and .NET both emit lowercase, and SQL Server renders uppercase.

What actually causes incidents is mixing spellings inside one column. String equality fails between f81d4fae-7dec-… and F81D4FAE7DEC… even though they are the same identifier, and whether your database hides that depends on collation rather than on anything in your code. Normalise at the boundary where values enter, store one spelling, and never normalise on read — a read-side conversion means two spellings already exist in the table.

The generator on this site emits the canonical lowercase form and has toggles for the unhyphenated and uppercase spellings, so you can produce the shape your column expects rather than editing values by hand. The same generator is also a Chrome extension with the same three toggles and a count field for seeding fixtures; it runs offline and declares no permissions.

Frequently asked questions

Which textual form does RFC 9562 actually define?

One, and it is narrow. Section 4 lays out the canonical form as 32 hexadecimal digits in five hyphen-separated groups of 8-4-4-4-12, and it says that generated output should use lowercase characters while software reading a UUID should accept either case. That is the whole of the specification's position on spelling. The braced form, the URN form and the unhyphenated form are not alternative canonical forms — they are conventions that individual platforms grew, and the specification neither blesses nor forbids them. So a parser accepting them is being generous rather than conformant, and a parser rejecting them is not being broken. That is precisely why two standard libraries can both be correct and still disagree about the same string.

Where do Python and .NET disagree?

In three places, and not in the same direction. We ran seven spellings of one UUID through Python 3.13.5's uuid.UUID() and .NET 10.0.400's Guid.TryParse. Both accept the canonical form, uppercase, the unhyphenated 32-character form, and the braced form. Python accepts the urn:uuid: prefix and .NET rejects it. Python accepts a string whose hyphens sit in the wrong positions and .NET rejects it. .NET accepts a parenthesised form and Python rejects it. So neither library is a superset of the other, and code that validates input in one language and parses it in another has a gap in both directions rather than a single conversion to handle. That matters most at a service boundary, because the obvious mitigation — validate with the stricter parser — does not exist here.

Why is Python accepting misplaced hyphens a problem?

Because it lets malformed data into your system that nothing downstream will accept. Python's parser strips the urn:uuid: prefix, strips braces, and then removes every hyphen wherever it occurs, so f81d4fae7dec-11d0-a765-00a0c91e6bf6 parses happily even though the groups are 12-4-4-12 rather than 8-4-4-4-12. We confirmed it returns the correct UUID. If that string arrived from a client and Python was your validation layer, you would store it, log it, and pass it on — and the first consumer with a stricter parser rejects it. The failure surfaces far from where the bad value entered. Validate the shape with a regular expression before parsing when the value came from outside your system.

Do Node and Go have this problem?

They avoid it by having no parser at all, which is its own kind of answer. Node's crypto module exports randomUUID and nothing else UUID-related — we checked the export list, and there is no parse function and no validator. Go's standard library has no UUID package whatsoever; the widely used implementations are third-party. So in both of those languages the acceptance rule is whatever you or your dependency wrote, which means it is usually a regular expression someone typed from memory. That is worth knowing before assuming a shared definition exists across your services. A UUID that crosses four languages may be validated four different ways, and only two of them came from a standard library.

Should I store UUIDs with or without hyphens?

Store them in whatever form your column was declared for, and normalise at exactly one boundary. The canonical 36-character form fits CHAR(36); stripping the hyphens gives 32 characters and fits CHAR(32), saving four bytes per row and losing nothing, because the hyphens carry no information. Both parsers we measured accept the unhyphenated form, so reading it back is safe. The real risk is mixing the two in one column, because string equality then fails between rows that hold the same identifier. Pick one spelling, convert on the way in, and never convert on the way out. Case matters for the same reason: both libraries emit lowercase, but SQL Server renders uppercase, so a cross-system string comparison can fail on identical values.

References

For which version to generate in the first place, see UUID v4 vs v7; for why v7 values can still sort wrong inside a single millisecond, see UUID v7 monotonicity.