The crosswalk¶
Level: reference · for anyone using more than one of these libraries
One line: One idea per row, and what each language does with it — so a concept you already understand in ABAP or Rust can be looked up rather than relearned.
These libraries were written separately and overlap on purpose: text handling is the place where a working programmer meets the same problem in every language and gets a different answer each time. This page is the index across them. It states the idea once, then names what each language calls it, so you can start from the column you already know.
A cell in the Python column links into this library. A cell in another column links into that library, or names the API when there is no page for it yet — an unlinked cell is a real answer with no page behind it, and often a gap worth filling. Stubs are deliberately not linked.
The ABAP column is not machine-checked — CI cannot run ABAP, and the ABAP library is one page long. Treat those cells as the name to search for, not as verified behaviour. Every Python, Rust and C cell either links to a page whose output is recorded, or states something checked while writing this one.
Text and bytes¶
| The idea | Python | Rust | C | ABAP |
|---|---|---|---|---|
| Text vs. raw bytes as separate types | str / bytes |
String / Vec<u8> ↗ |
no distinction — char * is both |
string / xstring ↗ |
| Converting between them | .encode() / .decode() |
String::from_utf8 ↗ / .as_bytes() ↗ |
hand-rolled, or iconv |
cl_abap_conv_codepage |
| Making one from scratch | bytes(n) / bytes(s, enc) / bytes([…]) — one name, four jobs |
vec![0u8; n] / .as_bytes() / vec![1, 2, 3] — three names |
{0} for the zeros; a char * already is the bytes |
xstring literal, or cl_abap_conv_codepage |
| What happens on invalid input | UnicodeDecodeError, or an errors= policy |
from_utf8 returns Result ↗; from_utf8_lossy substitutes ↗ |
undefined — nothing checks ↗ | exception, or a replacement char |
| The "never fails" escape hatch | surrogateescape |
none in std — the type will not hold it | — | — |
| Guaranteed-valid text type | str (code points, may hold lone surrogates) |
String — UTF-8, enforced by the type ↗ |
— | string (UTF-16 internally) |
The sharpest difference is the third row. Rust makes invalid UTF-8 unrepresentable in a String, so the check happens once at the boundary and never again. Python checks at the boundary too but leaves you a way through it (surrogateescape), because a filename has to be openable even when it is not text. C checks nowhere. Which of those is right depends entirely on whether your program can refuse its input.
Writing a literal down¶
| The idea | Python | Rust | C |
|---|---|---|---|
| What the prefix decides | type, escaping and interpolation — three separate questions | b r c — type and escaping; no interpolation prefix |
u8 u U L — the element type |
| How many prefixes | nine legal, eight two-letter combinations rejected | b, r, br, and c for C strings |
four, plus R"(…)" in C++11 |
| A hex escape's width | \xNN — exactly two, never greedy |
\xNN — exactly two, and ≤ 0x7F in a str |
\x… — unbounded, eats every hex digit |
| Naming a code point | \uXXXX, \UXXXXXXXX, \N{NAME} |
\u{…} — braced, so width never arises |
\uXXXX, \UXXXXXXXX |
| Octal | \NNN — one to three, greedy |
none at all | \NNN — one to three, greedy |
| An escape the language does not know | kept, and a SyntaxWarning since 3.12 |
compile error | warning, then implementation-defined |
| Adjacent literals | joined by the compiler | syntax error — use concat! |
joined by the compiler |
| Non-ASCII in a binary literal | b'é' is a SyntaxError |
b"é" is a compile error |
fine — it is just bytes |
The row with consequences is the sixth. Python keeping an unknown escape is what lets '\d' work in a regex and '\b' silently not, since \b is a real Python escape; Rust rejecting it means the same mistake cannot compile. The third and fourth rows are three answers to one question — fix the width, delimit it, or let it run — and only the middle one has no failure mode.
Showing a value¶
| The idea | Python | Rust | ABAP |
|---|---|---|---|
| Show it to a person | str(x), f'{x}' |
Display, {} |
WRITE |
| Show it to a programmer | repr(x), f'{x!r}', f'{x = }' |
Debug, {:?} ↗ |
the Debugger, plus a hex view |
| …with non-ASCII escaped | ascii(x), f'{x!a}' |
escape_default ↗ |
— |
A bytes value |
repr(b) — b'…', which is also what >>> shows |
escape_ascii() ↗ — the same inside on 254 of 256 bytes; both quotes always escaped |
WRITE of an XSTRING — hex |
| What happens with no "person" form | falls back to repr — silently |
will not compile: Display is not implemented |
— |
| "Printable" | str.isprintable() — what repr will not escape |
no public predicate; is_control is a narrower question |
CO against a set you wrote |
| A container's elements | always repr |
always Debug |
— |
Row four is the one with consequences. Python's str() falling back to repr() is what makes str(b'Zoot!') return "b'Zoot!'" instead of raising, and Rust's refusal to fall back is what makes the equivalent a compile error. Both languages define "printable" as "what the debug form does not escape", and on every sample tested they agree about which characters those are.
Making a column line up¶
| The idea | Python | Rust | ABAP |
|---|---|---|---|
| Pad to a width | ljust / rjust / center, or '{:<10}' — five methods and five specs |
format!("{:<10}") ↗ — one spelling only |
nothing to call: a c LENGTH 10 field is already padded |
| Zero-pad a number | zfill on a str, '{:05d}' on an int — and '{:05}' on a str is neither |
format!("{:05}"), sign-aware the same way |
UNPACK, or the numeric edit masks |
| Truncate to a width | '{:.10}' — the precision slot, never the width |
format!("{:.10}"), counted in chars |
the assignment itself truncates, silently |
| Expand tabs to columns | str.expandtabs(n) — arithmetic, not replacement |
none in std | — |
| Where the width is decided | in the call, every time | in the call, every time | in the type, once |
| What the width counts | code points | chars |
characters |
| Cells the result occupies | unicodedata.east_asian_width is the only data; no function computes it |
none in std ↗ | — |
The last two rows are the failure this whole section is about: every language here pads by counting characters, and every reader is looking at cells. Row five is the difference worth carrying between them. ABAP puts the width in the type, so a short value is padded and a long one is truncated at the assignment; Python and Rust put it in the call, where the width is a floor and nothing is ever cut. The two failure modes are opposites — silent data loss against a silently crooked table — and moving between the languages means expecting the wrong one.
C# is the same grammar as Python's with the slots swapped: "{0,-20} {1,5:N1}" puts the alignment before the colon, inside the field, where Python puts it after, inside the spec — '{0:<20} {1:5.1f}'. Read a .NET format string as a Python one and the , reads as a thousands separator instead.
Mutable and immutable¶
| The idea | Python | Rust | C | ABAP |
|---|---|---|---|---|
| The frozen binary type | bytes |
&[u8] — and b"abc" is a &[u8; 3] |
none; nothing is frozen | xstring — a value, copied on assignment |
| The one you can write into | bytearray |
Vec<u8> ↗ behind a mut binding |
char buf[N] |
the same xstring |
| Where mutability is written down | in the type — a Python name has no mut |
in the binding, and in & against &mut |
nowhere | nowhere |
| A literal you can edit | none — the literal is the bytes |
none — b"abc" is behind a & ↗, and the write does not compile |
char *p = "abc" compiles; writing is undefined behaviour |
— |
| A borrowed view, no copy | memoryview |
&mut [u8] |
a pointer, unchecked | a field symbol (ASSIGN) |
| Letting a callee fill your buffer | readinto / recv_into / pack_into |
read_exact(&mut buf) |
read(fd, buf, n) |
— |
| Two names for one buffer | assignment aliases; the callee can edit yours | only through &mut, and only one at a time |
any two pointers | not by assignment — ASSIGN or REF TO |
| Writing past the end | IndexError |
panics | corrupts whatever was next | — |
| What mutability costs | unhashable — no dict key, no set member | nothing at runtime; the borrow checker charges at compile time | nothing, and no safety either | — |
Python is the odd one out in the third row, and the rest of the column follows from it. A Python name cannot be declared mutable or not, so the only place left to record the difference is the object — which is why Python needs two types where Rust needs one Vec<u8> and a keyword. Rust and Swift put it on the binding; Go and C put it nowhere, and Go gets away with it only because string is frozen and []byte(s) is an honest copy. C's version of this row is the bug the other four languages were designed against: writing through char *p = "abc" compiles cleanly and dies at runtime.
Length and indexing¶
| The idea | Python | Rust | ABAP |
|---|---|---|---|
| Length in bytes | len(s.encode("utf-8")) |
s.len() ↗ — bytes, always |
xstrlen( ) ↗ |
| Length in UTF-16 units | len(s.encode("utf-16-le")) // 2 |
s.encode_utf16().count() ↗ |
strlen( ) — this is ABAP's default |
| Length in code points | len(s) |
s.chars().count() ↗ |
— (strlen( ) counts UTF-16 units) |
| Length a reader would agree with | not in the stdlib | not in std — needs a crate | — |
| Indexing by position | s[0] gives a 1-char str |
s[0] does not compile ↗ |
s(0) gives a code unit |
| Taking a sub-range | s[3:8] — start and stop, in code points |
&s[3..8] ↗ — start and end, in bytes |
s+3(5) — offset and length, in code units |
| An out-of-range bound | clamped to the length; never raises | panics ↗ — or get() returns None |
raises |
| A cut inside a character | impossible — the unit is a code point | panics: not a char boundary ↗ | possible above the BMP — half a surrogate pair |
| Counting back from the end | s[-5:] — and s[:-0] is '' |
no negative index — &s[s.len() - 5..] |
no negative offset — arithmetic on strlen( ) |
| Reversing | s[::-1] — code points, so it breaks a combining mark |
.chars().rev().collect() — code points, same flaw |
no notation; loop and concatenate |
| Walking it one unit at a time | for c in s — code points |
.chars() / .bytes() / .char_indices() ↗ |
offset arithmetic on code units |
Rust's refusal to index a string by integer is the design decision that most annoys newcomers and most reliably prevents the bug: s[0..1] on a multi-byte character panics rather than returning half a character. Python returns a whole code point, which is right more often than C and still not the same as a character. ABAP returns a UTF-16 code unit, so an emoji is two.
The three sub-range rows are the sharpest disagreement in the table, because all three languages write something that looks like the same operation and none of them mean it. Python's second number is a stop, ABAP's is a length, and Rust's pair is a byte range — so s[3:8], s+3(5) and &s[3..8] select the same five characters only while the text is ASCII. Then the contracts differ too: Python clamps an out-of-range bound and hands back a short string, ABAP raises, Rust panics — which puts Python's slice on the opposite side of the line from both, and is why s[100:200] on a five-character string is a silent bug here and a loud one there.
Ordering and comparison¶
| The idea | Python | Rust | ABAP |
|---|---|---|---|
What == actually compares |
code points, and only those — ordinal | UTF-8 bytes ↗ — same order, other end | the internal representation; a c field's trailing blanks fall out |
| Choosing a comparison mode | there is nothing to choose — one mode, no parameter | same, plus eq_ignore_ascii_case for the ASCII case |
the operator decides: CS/CP ignore case, CO/CA do not |
| Default sort order | code point | code point (Ord on str) ↗ |
UTF-16 code unit |
| Alphabetical for a real language | locale.strxfrm, or ICU |
needs a crate | collation-aware compare, or a sort key column |
| Same-looking strings comparing unequal | normalization | same problem, same fix | same problem |
| Case-insensitive comparison | str.casefold() — and not str.lower() |
str::to_lowercase ↗ — no folding in std at all |
TRANSLATE ... TO UPPER CASE on both sides |
| A NUL inside the string | three distinct strings; locale.strxfrm raises |
distinct — \0 is an ordinary char |
— |
| ASCII-only shortcut | s.lower() has none — it is always Unicode |
eq_ignore_ascii_case ↗ — cheap, and silently skips Ł |
— |
All three languages get the ordering rows equally wrong by default, and for the same reason: code-point order is the only ordering available without a locale database. Those are the rows where "Python's answer" is not really Python's — it is everyone's.
The second row is where they genuinely differ, and it is a three-way split rather than a spectrum. .NET makes the mode a parameter on every string API, so the choice is visible at each call site and unavoidable even when you have no opinion. ABAP makes it the operator, so two comparisons one letter apart in the source are two different modes. Python makes it nothing at all — there is one comparison, it is ordinal, and the cost is that a programmer can use it for years without learning there was a question. Comparison has a mode measures what each of Python's near-substitutes actually does.
Classifying a character¶
| The idea | Python | Rust | ABAP |
|---|---|---|---|
| Is it a letter? | str.isalpha() — General_Category L* |
char::is_alphabetic ↗ — the Alphabetic property, which is wider |
CO against a character set, by hand |
Is it a digit you can int()? |
str.isdecimal() — Nd |
char::to_digit(10).is_some() — ASCII only |
CO '0123456789' |
| Is it a number of any kind? | str.isnumeric() — Numeric_Type, so 一 counts |
char::is_numeric — N* categories, so 一 does not |
— |
| Whitespace | str.isspace() — Unicode, plus \x1c–\x1f |
char::is_whitespace ↗ — White_Space only |
cl_abap_char_utilities constants |
| Whole-string version | 12 methods on str; the ten class predicates also mean "…and not empty" |
only three — is_ascii ↗, is_empty ↗, is_char_boundary ↗ — else you write .chars().all(…) |
CO is already whole-field |
| The empty string | False for the ten class predicates |
true — all() over nothing |
worth checking on your system |
| Which edition of the table answered | unicodedata.unidata_version |
whatever rustc was built with |
the system code page |
This is the one section where the names match and the sets do not, in both directions: Rust's is_alphabetic accepts combining marks that Python's isalpha rejects, and Python's isnumeric accepts a CJK ideograph that Rust's is_numeric rejects. The measured grid is on Is it a letter?. The last row is the general form of the hazard — every one of these is a table lookup whose answer depends on which edition your toolchain was built against — and it has its own page: the table has a version ↗.
Changing case¶
| The idea | Python | Rust | ABAP |
|---|---|---|---|
| Uppercase a whole string | str.upper() |
str::to_uppercase ↗ — allocates a String |
TRANSLATE lv TO UPPER CASE — in place, no return value |
| Uppercase one character | s[i].upper() — still a str, of any length |
char::to_uppercase ↗ — returns an iterator, because one char in is not one char out |
— |
| A key for caseless comparison | str.casefold() — Default Case Folding |
not in std — a crate ↗ | — |
| The cheap ASCII-only shortcut | none — every case method is Unicode | eq_ignore_ascii_case ↗, to_ascii_uppercase ↗ — correct for protocol tokens, wrong for names |
— |
Language-tailored casing (Turkish ı/i) |
not available — the methods take no locale | not in std — icu_casemap |
depends on the system |
| Positional forms (Greek final sigma) | str.lower() does it; a per-character loop does not |
str::to_lowercase does it ↗; char::to_lowercase does not |
— |
| Title case | str.title(), string.capwords(), str.capitalize() — three answers that disagree |
none — you write it | — |
The row that carries the most information is the second one. Rust's char::to_uppercase returns ToUppercase, an iterator, because ß uppercases to two characters and a char cannot hold two; Python returns a str, which can hold any number, so the same fact is true and invisible. Both languages then split the same way on the sigma row: only the whole-string method can see a letter's neighbours, so only it gets the positional form right. C is the language where the fact is not merely hidden but unrepresentable — int toupper(int) is one value in and one value out, so the correct answer for ß cannot be returned at all, and the wide-character towupper has the same shape.
Taking a string apart, and putting one together¶
| The idea | Python | Rust | ABAP |
|---|---|---|---|
| Split on a separator | s.split(sep) |
str::split ↗ — lazy, returns an iterator |
SPLIT ... AT ... INTO TABLE |
| Split on whitespace | s.split() — no argument |
str::split_whitespace ↗ |
CONDENSE, then SPLIT |
| Split into lines | s.splitlines() |
str::lines ↗ — and what counts as a line ending ↗ |
SPLIT AT cl_abap_char_utilities=>newline |
| Which characters end a line | ten — \n \v \f \r \x1c \x1d \x1e \x85 U+2028 U+2029, plus \r\n |
one, plus a trailing \r trimmed — a lone \r is data |
whichever constant you named; there is no default |
| …when reading a file instead | three — universal newlines gives \n, \r, \r\n |
the same as lines(); BufRead does not have a second rule |
OPEN DATASET … IN TEXT MODE — platform-dependent |
| Split once, keep the rest | s.split(sep, 1) |
str::split_once ↗ — returns an Option of the pair |
— |
| What an empty separator does | ValueError |
"" splits at every char boundary ↗ — and yields empties at both ends |
— |
| Join | sep.join(parts) |
parts.join(sep) ↗ — the receiver is the slice, not the separator |
CONCATENATE ... SEPARATED BY |
| Build up in a loop | "".join(list), or io.StringIO |
String::push_str ↗ — with with_capacity when you know the size |
CONCATENATE in a loop |
| Find a substring | s.find / s.index — character offset |
str::find ↗ — byte offset, and Option rather than -1 |
FIND ... IN, sy-fdpos |
| …and what a miss looks like | -1 from find, ValueError from index — and -1 is a valid index, so an unchecked miss slices out the last character |
None ↗ — there is no integer to misuse until it has been unwrapped |
sy-subrc <> 0; the offset target is a separate variable |
| Cut at the first / last separator | s.partition / s.rpartition — always a 3-tuple, so the unpack never raises; the empty separator field is the only sign of a miss |
split_once / rsplit_once ↗ — Option of the pair, and no separator field to read |
SPLIT ... AT ... INTO a b |
| Is it in there at all? | sub in s — and on bytes an int is a needle: 115 in b'spam' is True |
str::contains ↗ |
CS, which also sets sy-fdpos |
| Replace | s.replace(a, b) |
str::replace ↗, replacen ↗ for a count |
REPLACE ALL OCCURRENCES OF |
| Substitute many characters at once | s.translate(table) — a dict keyed by ordinal; values of any length, None deletes |
nothing in std — chars().map(…) with a match, or flat_map to expand and delete |
TRANSLATE … USING 'axbycz' — positional pairs, in place, one char in and one char out |
| …and the trap it avoids | one pass over the original, so no substitution sees another's output | str::replace ↗ chained has the same bug — "abc" shifted by one collapses to "ddd" |
REPLACE ALL OCCURRENCES chained, same hazard |
| Delete a set of characters | s.translate({ord(c): None for c in set}), or maketrans's third argument |
s.replace(&['-','b'][..], "") — a Pattern may be a char set, so this is one call |
REPLACE ALL OCCURRENCES OF … WITH '', per character |
| Trim the ends | s.strip() — 29 code points, not four |
str::trim ↗ — White_Space, so it leaves U+001C–U+001F that Python removes; trim_ascii is the cheap one |
CONDENSE, or SHIFT ... LEFT DELETING |
| Trim a set of characters | s.lstrip(chars) — the argument is a bag, and it repeats |
trim_start_matches(&[char]) — a &str pattern means the whole string, so you must spell the set out |
SHIFT … DELETING LEADING takes a mask |
| Prefix and suffix | s.startswith, s.removeprefix — once, and silent when it did not match |
starts_with ↗, strip_prefix ↗ — returns Option, so "not there" is a value |
CP with a pattern |
| Repeat | s * 3 |
str::repeat ↗ |
DO 3 TIMES, CONCATENATE |
| Parse into a number | int(s) / float(s) |
s.parse::<T>() ↗ — the type decides the parser |
MOVE, with silent conversion |
| Interpolate values in | f-strings, format(), str.format — one grammar, four doors |
format! ↗ — the same spec string produces the same bytes; a macro, so a runtime template does not compile |
string templates, options named rather than punctuated |
| The older spelling | '%s' % x — one operator, one operand, and the only one that works on bytes |
— | — |
| A literal with no escapes | r"..." — but the backslash still ends the literal, so it cannot end in one |
r#"..."# ↗ — the hashes let you nest quotes, and r"C:\Users\" compiles |
— |
The substitution rows are the sharpest three-way split in the table. Python has a table method, so a many-to-many substitution is one pass and has no ordering; Rust has no such method, so the same job is chars().map() and the chained-replace bug is available in both languages with a fix in only one; ABAP has the ancestor — TRANSLATE … USING — and it is positional, in place, and cannot delete or expand. The shell's tr is the same idea again with the ordinals removed, which is why it works on bytes and mangles UTF-8: tr and sort work a byte at a time ↗.
Two things run through the whole table. Rust's versions are lazy and byte-indexed: split hands back an iterator you can stop consuming, and find gives a byte offset you must not treat as a character position — which is the same trap as indexing, one method along. And Rust returns Option where Python returns a sentinel or raises: find gives None rather than -1, split_once gives None rather than a one-element list, so the failure is in the type instead of in the docs. The Python column here is thin on links on purpose — this library's chapter 1 is about the text model, and the method-by-method tour is one of the gaps this page is meant to make visible. Two of those gaps are now filled by pages that group methods by how they fail: strip is a set and four ways to find it.
Opening, reading and replacing a file¶
| The idea | Python | Rust | C | ABAP |
|---|---|---|---|---|
| Open for reading | open(p, encoding="utf-8") |
File::open ↗ — bytes, no encoding argument exists |
fopen(p, "r") |
OPEN DATASET … FOR INPUT IN TEXT MODE ENCODING UTF-8 |
| Open for writing, destroying what is there | open(p, "w") — at open(), not at the first write |
File::create — same moment, same surprise |
fopen(p, "w") |
… FOR OUTPUT |
| Create, or fail if it exists | open(p, "x") |
OpenOptions::create_new |
fopen(p, "wx") — C11 only |
no direct form; test first |
| Is there a text mode? | always — every read decodes | no such thing — a File is bytes |
on Unix, text mode is binary mode | IN TEXT MODE ENCODING …, opt-in |
| Where a file position comes from | tell() — an opaque cookie, 39 digits for a 6-byte file |
Seek::seek — a byte offset, always |
ftell — a byte offset in practice, "unspecified" in text mode by the standard |
GET DATASET … POSITION — bytes |
| Line endings, reading a file | three — \n, \r, \r\n, all delivered as \n |
one — \n, with a trailing \r trimmed |
one — \n, and fgets keeps it |
platform-dependent; there is no default |
| Turning that translation off | newline="" |
nothing to turn off | nothing to turn off | IN BINARY MODE |
| stdout when it is a pipe | block-buffered — stderr overtakes it | line-buffered anyway — io::stdout() is a LineWriter |
block-buffered | no equivalent |
| Forcing it out | flush=True, -u, PYTHONUNBUFFERED |
flush(), and BufWriter makes it visible |
fflush, setvbuf |
CLOSE DATASET |
| Replace a file atomically | os.replace — and it does not carry the old mode |
fs::rename |
rename — POSIX guarantees it, ISO C does not |
no rename statement |
The row that decides the shape of every other one is the fourth. Rust has no text mode, so it has no cookie, no newline translation and no encoding default — three of Python's traps are absent because one layer is. C has a text mode on paper and not on Unix, which is why its ftell rule is the same rule as Python's and almost no C programmer has met it. And the last row is where ABAP is genuinely short: without a rename, the write-beside-and-rename idiom is unavailable, so a file really is rewritten in place.
Numbers, bytes and hex¶
| The idea | Python | Rust | C | Where the idea itself lives |
|---|---|---|---|---|
| A byte | int 0–255, or one element of bytes |
u8 ↗ |
unsigned char |
A byte is eight bits ↗ |
| A number that is not a byte | bytes([256]) raises ValueError — nothing wraps |
a literal will not compile; 300 as u8 is 44, u8::try_from gives Err |
(unsigned char)300 is 44 — a warning at most |
A byte is eight bits ↗ |
| Writing one down in hex | 0xFF, hex(n) |
0xFF, {:#x} ↗ |
0xFF, %x |
Hex is a shorthand ↗ |
| A negative number in binary | bin(-9) is '-0b1001' — a sign and a magnitude |
{:#b} on -9i8 is 0b11110111 — the type's width shows through |
no conversion before C23's %b, and macOS's printf still prints %b as a plain b |
A byte is eight bits ↗ |
| Digit separators in a literal | 1_000_000 |
1_000_000 ↗ |
1'000'000 — C++14, and C23; rejected by C17 |
— |
| Bytes to a hex string and back | b.hex() / bytes.fromhex() |
{:02x} per byte; hex crate to parse |
printf("%02x") |
Reading a hex dump ↗ |
Is 414243 a number or three bytes? |
depends entirely on the call you make | same — the type says which | same | Hex: number or bytes? ↗ |
| Integer ↔ bytes, with an endianness | int.to_bytes(n, "big") |
u32::to_be_bytes / from_be_bytes |
cast, or htonl |
Byte order and the BOM ↗ |
| Bit flags in one integer | enum.IntFlag |
bitflags-style constants ↗ |
#define and bitwise or |
— |
| What a float actually stores | float is IEEE 754 double |
f32 / f64 ↗ |
float / double |
— |
| Overflow | int is arbitrary-precision — no overflow |
panics in debug, wraps in release — checked_ / saturating_ ↗ |
undefined for signed | — |
The fourth column is the one to notice: these are the rows where the idea is language-independent, so the encodings library owns it and all three languages are just spellings. The last two rows are where they genuinely differ — Python's integers do not overflow because they are not machine words, which removes a whole class of bug and a whole class of performance guarantee at the same time.
Projects, dependencies and pinning¶
| The idea | Python | Rust |
|---|---|---|
| The manifest | pyproject.toml, [project] |
Cargo.toml ↗ |
| The resolved lockfile | uv.lock — commit it for an app |
Cargo.lock ↗ — commit it for a binary, not for a library |
| Dev-only dependencies | [dependency-groups] (PEP 735) |
[dev-dependencies] — same idea, older |
| Opt-in extras | [project.optional-dependencies] |
features — and they are additive, which is the trap |
| Two versions of one package | impossible in one environment | allowed, if the majors differ ↗ — and the type from one is not the type from the other |
| Several packages, one resolve | [tool.uv.workspace] |
[workspace] ↗ — Cargo had it first; uv borrowed the word |
| Pinning the toolchain itself | requires-python, .python-version |
rust-toolchain.toml ↗ — the file the whole team gets |
| Building offline | wheels in a local index | cargo vendor + [patch] ↗ |
| Where a tool keeps its settings | [tool.<name>] in the same file |
[package.metadata.<name>], or the tool's own file |
Cargo and uv look alike because one copied the other, and the vocabulary is worth learning once: manifest, lock, workspace, feature. The row that does not translate is the fifth — Python has one version of a package per environment and a conflict is a hard error, while Cargo will happily link two majors of the same crate and let you discover it when a type from one will not go where the other's is expected.
Where each library goes deeper¶
- Encodings library ↗ — the subject itself: what a code point is, how UTF-8 encodes one, byte order and the BOM, overlong sequences, mojibake, and the terminal tools (
od,xxd,iconv) that show you the bytes. Read it when the question is what is actually in the file. - Rust library ↗ —
Stringvs&str,char, slicing by byte, and what the type system buys. Read it when the question is why won't this compile. - ABAP library ↗ — the SAP side, where code pages are configuration rather than a literal.
- This library — Python's answers, and the places Python's answer is unusual.