demangle
Read mangled symbol names in pure Python — Itanium C++ (GCC/Clang), MSVC, Rust, Swift, Objective-C, Go, D, Nim, Free Pascal, Delphi, Ada/GNAT, JNI, and the pre-Itanium C++ families (g++ 2.x, cfront/ARM, Lucid, HP aCC, CodeWarrior). No dependencies, no native code, no compiler required.
>>> import demangle
>>> demangle.demangle("_ZNSt6vectorIiSaIiEE9push_backERKi")
'std::vector<int, std::allocator<int>>::push_back(int const&)'
>>> demangle.demangle("?f@@YAXH@Z")
'void __cdecl f(int)'
>>> demangle.demangle("_ZN4core3fmt9Formatter3pad17h9b2b3a0e5b4d1b31E")
'core::fmt::Formatter::pad'
>>> demangle.demangle("$s10Foundation4DataV5countSivg")
'Foundation.Data.count.getter : Swift.Int'
>>> demangle.demangle("example.com/m/v2%2e5.(*T).Method", language="go")
'example.com/m/v2.5.(*T).Method'
>>> demangle.demangle("eqdestroy___systemZassertions_23")
'system/assertions.=destroy'
>>> demangle.demangle("MYUNIT$_$TWIDGET_$__$$_AREA$$LONGINT")
'MYUNIT.TWIDGET.AREA: LONGINT'
>>> demangle.demangle("@Unit@Class@Method$qqrv")
'__fastcall Unit::Class::Method()'
>>> demangle.demangle("AddAlignment__9ivTSolverUiP12ivInteractorP7ivTGlue")
'ivTSolver::AddAlignment(unsigned int, ivInteractor *, ivTGlue *)'
>>> demangle.demangle("__dt__6CActorFv")
'CActor::~CActor()'
- No toolchain. Every other option in Python binds to a native demangler —
cxxfiltandpycxxfiltwrap LLVM or libstdc++,undnamewraps Wine through CFFI — which means a C compiler at install time, a platform-specific wheel, and for MSVC nothing maintained at all. This is Python and nothing else. - A tree, not just a string.
parse()answers with nodes to walk, so the namespace, the template arguments and the parameter types are fields rather than a regular expression against C++ declaration syntax, which nests and so cannot be parsed that way. - Measured, not asserted. Every scheme is scored against the reference demangler for its mangling, over about 2,400,000 symbols out of shipped libraries, and every name that differs is accounted for by name. CONFORMANCE.md has the numbers and what each is measured against.
- Safe on untrusted input.
demangle()never raises, for any input; recursion depth, output size, substitution count and input length are all bounded, and every bound is configurable per call.
Install
pip install demangle
Python 3.13 or newer. That is the whole dependency list.
Use
The string you probably want
demangle.demangle(name) # never raises; returns `name` unchanged if unreadable
demangle.demangle_strict(name) # raises DemanglingError instead
demangle() is built for the case where you are labelling every symbol in a binary and
most of them are not mangled at all. A name it cannot read comes back exactly as it went
in, because a wrong expansion is worse than a mangled one — it matches neither the
symbol nor the declaration.
The structure, when you need it
>>> tree = demangle.parse("_ZNK3Foo3barIiEEvPKc")
>>> tree.spell()
'void Foo::bar<int>(char const*) const'
>>> [node.text for node in tree.find("name")]
['Foo', 'bar']
>>> function = next(tree.find("function"))
>>> len(function.parameters)
1
Every node supports .walk(), .find(kind), .children() and .spell().
All schemes return full trees. A Rust symbol comes back as a symbol holding a
path of name components, with impl, template, type and literal nodes for what
the path carries:
>>> tree = demangle.parse("_ZN4core3fmt9Formatter3pad17h9b2b3a0e5b4d1b31E")
>>> [node.text for node in tree.find("name")]
['core', 'fmt', 'Formatter', 'pad']
The parts, when the spelling is not what you want
signature() gives the pieces rather than the string — the base name without its
scope, the scope without the base name, the parameter types on their own:
>>> parts = demangle.signature("_ZNSt6vectorIiSaIiEE9push_backERKi")
>>> parts.namespace, parts.base_name
('std::vector<int, std::allocator<int>>', 'push_back')
>>> parts.parameters
('int const&',)
>>> demangle.signature("$s4main3FooV3baryS2i_SStF").return_type
'Swift.Int'
namespace, the scheme's separator and base_name spell qualified_name exactly, so
the three recombine. Splitting the string would not give that: :: and . and ,
occur inside template arguments and inside operator names as well as between components.
What each scheme records differs, and the fields say so rather than guessing. A C++ or
Swift name carries a signature; a Rust or Go path does not, and its parameters is
None — which is not (). is_function is False where the name does not say.
A type on its own, not a whole symbol
A typeinfo name, an RTTI type descriptor and a Swift metadata typeref all carry a
type rather than a symbol. demangle_type() reads one, and parse_type() gives its
tree:
>>> demangle.demangle_type("PKFvRiE", language="itanium")
'void (*)(int&) const'
>>> demangle.demangle_type(".PEAX", language="msvc") # a type descriptor's own symbol
'void *'
>>> demangle.demangle_type("SaySiG", language="swift")
'[Swift.Int]'
language is required, and that is not an oversight: a whole symbol announces its
scheme — _Z, ?, $s — and a type encoding announces nothing at all, so Si is
std::istream to the Itanium reader and Swift.Int to the Swift one. Every reference
tool puts this behind a flag for the same reason. On the command line it is
demangle --types -l itanium, one encoding per argument rather than a filter over mixed
text — Pi is an ordinary word, and I like Pi should stay as it is.
A file, not a name
nm writes an address and a type letter before a name; a crash log writes a frame
number. So the useful operation over a file is "substitute every symbol-shaped word and
copy the rest through", which is what the command does with no arguments — and now what
the library does too:
>>> demangle.demangle_text("0000000000001139 T _ZN3foo3barEv")
'0000000000001139 T foo::bar()'
>>> demangle.demangle_stream(sys.stdin, sys.stdout) # a line at a time, for pipes
>>> [f.mangled for f in demangle.find_symbols("a _ZN3foo3barEv b")]
['_ZN3foo3barEv']
find_symbols gives the span as well as the spelling, for a caller that needs to know
where in a line a symbol was.
Detection and batches
>>> demangle.detect("?f@@YAXH@Z")
'msvc'
>>> list(demangle.demangle_all(symbol_table)) # generator, shares the cache
Bytes, when the names came from a symbol table
An ELF or Mach-O string table holds bytes, and they are not reliably UTF-8 — a truncated table cuts a name mid-character. Every entry point has a bytes form, so reading one does not mean guessing an encoding first:
>>> demangle.demangleb(b"_ZN3foo3barEv")
b'foo::bar()'
demangleb_strict, detectb, parseb, signatureb, demangleb_type and
parseb_type go with it. Undecodable bytes
survive the round trip: demangleb hands back exactly what it was given, byte for byte,
rather than raising.
The tree as data
parse() returns a walkable tree; to_dict() turns it into plain data, --json prints
it, and node_kinds() is the vocabulary to switch on rather than something to read out
of our source:
>>> demangle.parse("_Z1fPi").to_dict()
{'kind': 'function', 'name': {'kind': 'name', 'text': 'f'}, 'parameters': [{'kind': 'pointer', 'inner': {'kind': 'builtin', 'spelling': 'int'}}], 'returns': None, 'suffix': ''}
>>> demangle.node_kinds("d")
('name', 'path', 'symbol')
The nodes carry __match_args__, so structural pattern matching works:
>>> from demangle.core.ast import Builtin, Pointer
>>> match demangle.parse("_Z1fPi").parameters[0]:
... case Pointer(Builtin(name)): print("pointer to", name)
pointer to int
A node reached more than once — an Itanium substitution, a Rust node named both by
position and by role — is written once with an id and afterwards as {"$ref": id}. The
structure is a graph, and expanding it in full does not always terminate in useful time.
Styles
The two reference demanglers legitimately disagree on some spellings. Both are available, and neither is wrong:
>>> demangle.demangle("_ZNSt6vectorIiSaIiEE9push_backERKi", style="llvm") # default
'std::vector<int, std::allocator<int>>::push_back(int const&)'
>>> demangle.demangle("_ZNSt6vectorIiSaIiEE9push_backERKi", style="gnu")
'std::vector<int, std::allocator<int> >::push_back(int const&)'
Printing less of a name
A decorated name expands to a great deal more than the name. style() composes what to
leave out, at the call site rather than by registering anything globally:
>>> demangle.demangle("?g@C@@UEAAXXZ")
'public: virtual void __cdecl C::g(void)'
>>> narrow = demangle.style("llvm", msvc={"calling_convention": False, "access_specifier": False})
>>> demangle.demangle("?g@C@@UEAAXXZ", style=narrow)
'virtual void C::g(void)'
The MSVC scheme has nine such options: the five flags llvm-undname has —
calling_convention, access_specifier, member_type, return_type, variable_type
— and four UnDecorateSymbolName mask bits it has no flag for, ms_keywords,
leading_underscores, this_type and tag_kind. The two kinds reach differently, which
is the references' doing rather than a convenience:
MsvcOptions says which is which, field by
field, and each kind is scored against its own reference in
CONFORMANCE.md. Every one of them has a command-line flag.
Swift has the bundle Xcode and LLDB show instead of the full spelling — Either for
Monads.Either, (_:) for (Swift.Int) -> Swift.UInt, specialized f() for a page of
specialisation arguments. It is --simplified on the command line, and exact against
Swift's own 217 vectors:
>>> from demangle.schemes.swift import SIMPLIFIED_OPTIONS
>>> demangle.demangle("_TtFSiSu", style=demangle.style("llvm", swift=SIMPLIFIED_OPTIONS))
'(_:)'
Command line
$ nm -a libfoo.so | demangle
$ demangle _ZNSt6vectorIiSaIiEE9push_backERKi
$ demangle --tree _Z1fPKc
$ demangle --detect _RNvC6_123foo3bar
$ demangle -p _ZNSt6vectorIiSaIiEE9push_backERKi # the name, without the signature
$ demangle --base-name _ZSt4sortIPiEvT_S1_ # `sort<int*>`
$ demangle --no-return-type _ZSt4sortIPiEvT_S1_ # the declaration, minus `void `
$ demangle --ret-postfix _Z1fIiET_S0_ # `f<int>(int)int`, as `DMGL_RET_POSTFIX`
$ demangle --strip-underscore '_?f@@YAXH@Z' # ignore one leading underscore
$ demangle --keep-hash _ZN4core3fmt5write17h05af221e174051e9E # keep Rust's hash
$ demangle --types -l itanium PKFvRiE # a bare type, as `c++filt -t`
$ demangle --no-calling-convention '?f@@YAXH@Z' # `void f(int)`
$ demangle --no-tag-kind '?g3@@YAXVV@@@Z' # `void __cdecl g3(V)`
$ demangle --simplified _TtFSiSu # Swift, the way Xcode shows it
$ demangle --json _Z1fPi # the parse tree as JSON
Correctness
Correctness here is a measurement rather than a claim. Every scheme is scored against the
reference implementation for its mangling — llvm-cxxfilt, GNU c++filt,
llvm-undname, rustfilt, swift-demangle, cwdemangle, Embarcadero's own unmangler —
and, where no reference exists, against a property the mangling itself has to satisfy:
re-mangling what was read must reproduce the symbol the compiler wrote.
The checked-in corpora are replayed by the test suite with no compiler and no reference demangler present. Beyond them, whole symbol tables are run live against the references: about 2,400,000 real symbols from shipped libraries, and every name that differs is accounted for one by one.
CONFORMANCE.md is the whole picture — what each corpus is measured against, the notes behind every number, the live runs, and the places where following a reference would itself be the defect.
Safety
A mangled name is untrusted input in any tool that opens files it did not produce, so the bounds on depth, output size, substitution count and input length are the design rather than a hardening pass. Results are deterministic, and the property-based suite runs every parser against arbitrary text, mangling-alphabet text, arbitrary bytes, every truncation of a known-good name, and inputs built to blow up a naive parser. SECURITY.md has the threat model, what is in scope, and how to report privately.
Performance
Pure Python, measured on the conformance corpora (benchmarks/bench.py):
| Workload | Throughput |
|---|---|
| Cold — every name distinct | ~24,000 names/sec |
| Warm — names repeat, as in a real symbol table | ~2,200,000 names/sec |
| Non-mangled names rejected | ~510,000 names/sec |
| Full AST construction | ~16,500 names/sec |
Treat these as ratios rather than absolutes. The gap between cold and warm is the point: symbol tables repeat themselves relentlessly, and results are cached.
bench.py --check gates CI against the committed baseline. It compares figures normalised
against a calibration workload measured in the same run, so the gate reports a slower
demangler rather than a slower machine.
Architecture
The short version: parsers never build their own output. Each one is written against
a Builder protocol and reports the grammar productions it recognises; a fast text
builder or a tree builder decides what those become. That is what lets one parser serve
both demangle() and parse() with no second implementation to drift.
Schemes are plugins. core never imports one, they never import each other, and a
separate distribution can add a language through the demangle.languages
entry-point group without patching this package. Both rules are enforced by tests.
Working with the tree works
one real task through parse() end to end — finding every function in libstdc++ that
takes a string by const reference, and measuring what the regular-expression version of
the same question gets wrong.
See ARCHITECTURE.md for the full picture and Adding a scheme to add a language of your own. The API reference is published at https://r0ny123.github.io/demangle/.
Contributing
New schemes, corpus contributions, and conformance bug reports are all welcome — see CONTRIBUTING.md. A good bug report is a mangled name, what the reference prints, and what this library prints.
Fixing a spelling or adding a language should not require understanding the whole codebase. Where it does, that is a bug.
Licence
MIT. The Rust demangler derives from Team bi0s' rust_demangler (MIT) and the MSVC
demangler from SMDA (BSD 2-Clause); both are
substantially modified, and both upstream licences are reproduced in
NOTICE.
Metadata
Release files for demangle 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| demangle-0.3.0.tar.gz | 3.2 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| demangle-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 3.7 MB
Release files / demangle-0.3.0.tar.gz
| Download URL | demangle-0.3.0.tar.gz |
|---|---|
| Size | 3.2 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3d6f74055d8fc38cba941cbb8b60b40ad614b7bb4f5c481112f7ddff9548f3df
|
|
BLAKE2b-256 checksum How to use checksums |
d76535f47d9dfd4394f9023a76da59334a75f938ed8e082fa6f4724239f3a87b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.
Transparency logRelease files / demangle-0.3.0-py3-none-any.whl
| Download URL | demangle-0.3.0-py3-none-any.whl |
|---|---|
| Size | 492.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d61aecb1084c04d97b647b97015d665e3458d06a9e546a2bafa58692332e218f
|
|
BLAKE2b-256 checksum How to use checksums |
142464adf1c56a4ea2df1d9d0b27911387eba45949da284aded7e3451f50f8c9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.
Transparency log