Skip to content

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) StringUTF-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 reprsilently 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_numericN* categories, so does not
Whitespace str.isspace() — Unicode, plus \x1c\x1f char::is_whitespaceWhite_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 trueall() 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 CASEin 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::findbyte 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_onceOption 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 stdchars().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::trimWhite_Space, so it leaves U+001CU+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 anywayio::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 ↗String vs &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.