More units

Short entries for units that have no page of their own yet. Each one says what the unit is, gives an example, and lists the limits a user meets first. The examples were run with pin v451 (sha256 d9b7226769cc) from a release archive on x86-64 Linux on 2026-09-29. Where a unit stands in for a CPython module, the same program was run under CPython 3 and the outputs compared.

Units for Pascal:

Modules for Nil Python:

bignum: arbitrary-precision integers

TBigInt is a signed integer of any size, with the operators + - * div mod and the comparisons. Integers and strings come in through BigFromInt and BigFromStr, and results go out through BigToStr. BigDivMod and BigModPow cover division with remainder and modular powers.

program bn;
uses bignum;
var f, p, q, r: TBigInt; i: Integer;
begin
  f := BigFromInt(1);
  for i := 2 to 30 do f := f * BigFromInt(i);
  WriteLn(BigToStr(f));
  p := BigFromStr('-123456789012345678901234567890');
  BigDivMod(p, BigFromInt(97), q, r);
  WriteLn(BigToStr(q), ' ', BigToStr(r));
  WriteLn(BigToStr(BigModPow(BigFromInt(2), BigFromInt(1000), BigFromStr('1000000007'))));
  WriteLn(f > p, ' ', p < BigFromInt(0));
end.
265252859812191058636308480000000
-1272750402189130710322005854 -52
688423210
TRUE TRUE

Python's integers give the same four lines. Division truncates toward zero and the remainder takes the sign of the dividend, as div and mod do on Int64.

Limits:

examples/bignum computes large factorials and powers with it.

zlib: deflate and inflate

One unit serves both languages. From Pascal, DeflateZlib compresses a byte array and InflateZlib decompresses one. InflateGzip reads the gzip wrapper. From Nil Python, import zlib gives compress, decompress, crc32 and adler32. Its streams are ordinary zlib streams: CPython's zlib reads what it writes, and it reads what CPython writes.

program zp;
uses hashing, zlib;
var src, zipped, back: TByteArray; i: Integer; err: AnsiString;
begin
  SetLength(src, 4000);
  for i := 0 to 3999 do src[i] := Ord('a') + (i mod 7);
  DeflateZlib(src, zipped, 6);
  WriteLn(Length(src), ' -> ', Length(zipped));
  WriteLn(InflateZlib(zipped, back, err), ' ', Length(back) = Length(src));
end.
4000 -> 35
TRUE TRUE
import zlib

data = b"hello hello hello hello zlib" * 20
packed = zlib.compress(data)
print(len(data), len(packed) < len(data))
print(zlib.decompress(packed) == data)
print(zlib.crc32(data), zlib.adler32(data))
560 True
True
587555724 1067635221

CPython prints the same checksums for the same data.

Limits:

hashing, sha256 and sha512: checksums and digests

hashing has CRC32 (the polynomial PNG, zlib and gzip use) and Adler32, over a TByteArray. sha256 has SHA-256, HMAC-SHA256 and HKDF-SHA256, and sha512 has SHA-512. The SHA units take and return byte strings (AnsiString, one byte per character). A digest comes back as raw bytes, and Sha256Hex turns any byte string into lower-case hex, including a SHA-512 digest.

program hs;
uses sysutils, hashing, sha256, sha512;
var b: TByteArray; prk: AnsiString; i: Integer;
begin
  WriteLn(Sha256Hex(Sha256('abc')));
  WriteLn(Sha256Hex(HmacSha256('key', 'The quick brown fox jumps over the lazy dog')));
  WriteLn(Sha256Hex(Sha512('abc')));
  prk := HkdfExtract(#$00#$01#$02#$03#$04#$05#$06#$07#$08#$09#$0a#$0b#$0c,
                     StringOfChar(#$0b, 22));
  WriteLn(Sha256Hex(HkdfExpand(prk, #$f0#$f1#$f2#$f3#$f4#$f5#$f6#$f7#$f8#$f9, 42)));
  SetLength(b, 5);
  for i := 0 to 4 do b[i] := Ord('hello'[i + 1]);
  WriteLn(CRC32Bytes(b), ' ', Adler32(b));
end.
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8
ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f
3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865
907060870 103547413

CPython's hashlib, hmac and zlib give the same values. The HKDF line is test case 1 of RFC 5869. Hashing 1 MB with Sha256 took 0.13 s.

Limits:

png and image: reading and writing PNG files

image holds a bitmap: TImage has Width, Height and Pixels, an array of 8-bit TRGBA values in row order. png reads a PNG file's bytes into a TImage with PngDecodeRGBA, and writes one with PngEncodeRGBA. No compression library is needed.

program pe;
uses sysutils, hashing, image, png;
var img, back: TImage; x, y: Integer; bytes: TByteArray;
begin
  ImageInit(img, 64, 32);
  for y := 0 to 31 do
    for x := 0 to 63 do
      ImageSetPixel(img, x, y, MakeRGBA(x * 4, y * 8, 128, 255));
  PngEncodeRGBA(img, bytes);
  WriteLn(Length(bytes), ' bytes');
  WriteLn(PngDecodeRGBA(bytes, back), ' ', back.Width, 'x', back.Height, ' ',
          ImageGetPixel(back, 10, 3).R, ' ', ImageGetPixel(back, 10, 3).G);
end.
2513 bytes
TRUE 64x32 40 24

The encoder writes 8-bit RGBA (colour type 6), which every PNG reader accepts; the Python imaging library PIL read the file above back with every pixel equal. The decoder reads every non-interlaced colour type: files written by PIL as RGBA, RGB, grayscale, grayscale with alpha, palette, 1-bit and 16-bit grayscale all decoded to the same pixels PIL reports. Every result is widened to 8-bit RGBA: a 16-bit sample keeps its high byte. PngLastColourType and PngLastBitDepth report what the file was.

Limits:

strutils: FPC's string helpers, in part

26 routines from FPC's StrUtils: LeftStr, RightStr, MidStr, DupeString, PosEx, ReverseString, AddChar, AddCharR, the AnsiContains/AnsiStarts/AnsiEnds/AnsiReplace/AnsiIndex routines in their Str and Text forms, WordCount, ExtractWord, WordPosition, ExtractWordPos, ExtractDelimited, ExtractSubstr, IfThen and SplitString.

program su;
{$mode objfpc}{$H+}
uses sysutils, strutils;
var p: Integer; parts: TStringArray; i: Integer;
const s = 'the quick brown fox';
begin
  WriteLn(LeftStr(s, 3), '|', RightStr(s, 3), '|', MidStr(s, 5, 5), '|', DupeString('ab', 3));
  WriteLn(PosEx('o', s, 14), ' ', ReverseString('abc'), ' ', AddChar('*', 'x', 4));
  WriteLn(AnsiContainsText(s, 'QUICK'), ' ', AnsiReplaceText('A-b-a', 'a', 'x'),
          ' ', AnsiIndexStr('fox', ['the', 'fox']));
  WriteLn(WordCount(s, [' ']), ' ', ExtractWord(3, s, [' ']), ' ',
          ExtractDelimited(3, 'a,b,,c', [',']), '|');
  p := 1;
  Write(ExtractSubstr('k=v;x', p, ['=', ';']), ' ');
  WriteLn(ExtractSubstr('k=v;x', p, ['=', ';']), ' ', p);
  parts := SplitString('a b  c', ' ');
  Write(Length(parts), ':');
  for i := 0 to High(parts) do Write(' [', parts[i], ']');
  WriteLn;
end.
the|fox|quick|ababab
18 cba ***x
TRUE x-b-x 1
4 brown |
k v 5
4: [a] [b] [] [c]

FPC 3.2.2 prints the same, and a program calling all 26 routines printed the same as FPC on every line.

Limits: FPC's StrUtils has many more routines. Among the common ones, ContainsStr, StartsStr, EndsStr, ReplaceStr, ReplaceText, RPos, NPos, PosSet, DelSpace, PadLeft, StuffString, IntToRoman and Soundex are missing: a call to one does not compile.

dateutils: FPC's date helpers, in part

Seven routines from FPC's DateUtils: YearOf, MonthOf, DayOf, HourOf, MinuteOf, SecondOf and EncodeDateTime. TDateTime itself, Now, FormatDateTime, DayOfWeek and IsLeapYear are in sysutils, as in FPC.

program du;
{$mode objfpc}{$H+}
uses sysutils, dateutils;
var t: TDateTime;
begin
  t := EncodeDateTime(2026, 9, 29, 14, 5, 42, 250);
  WriteLn(YearOf(t), ' ', MonthOf(t), ' ', DayOf(t), ' ', HourOf(t), ' ', MinuteOf(t), ' ', SecondOf(t));
  WriteLn(FormatDateTime('yyyy-mm-dd hh:nn:ss', t));
  WriteLn(t:0:6);
end.
2026 9 29 14 5 42
2026-09-29 14:05:42
46294.587295

FPC 3.2.2 prints the same.

Limits: the rest of DateUtils is missing. IncDay, DaysBetween, DateTimeToUnix, UnixToDateTime, DayOfTheWeek and MilliSecondOf do not compile, for example.

collections: a generic list

generic TList<T>, a growable list with Add, Get, Put, Count and Clear. This is PXX's own small unit, not FPC's Generics.Collections, so there is no FPC program to compare against. The sums below are checked against the same arithmetic in Python.

program col;
{$define PXX_MANAGED_STRING}
uses collections;
type
  TIntList = specialize TList<Integer>;
  TStrList = specialize TList<AnsiString>;
var a: TIntList; s: TStrList; i, sum: Integer;
begin
  a := TIntList.Create;
  for i := 1 to 1000 do a.Add(i * i);
  sum := 0;
  for i := 0 to a.Count - 1 do sum := sum + a.Get(i);
  a.Put(0, -1);
  WriteLn(a.Count, ' ', sum, ' ', a.Get(0), ' ', a.Get(999));
  s := TStrList.Create;
  s.Add('alpha'); s.Add('beta');
  WriteLn(s.Count, ' ', s.Get(1));
  a.Clear;
  WriteLn(a.Count);
end.
1000 333833500 -1 1000000
2 beta
0

Limits:

contnrs: object lists

FPC's TFPObjectList and TFPHashObjectList: Add, Delete, Remove, Extract, IndexOf, Insert, Move, Exchange, Sort, the default Items[] property and OwnsObjects. The hash list adds Add(name, object), Find, FindIndexOf and NameOfIndex. Its lookup is a linear search, not a hash table: the results are FPC's, only slower on long lists.

program cnt;
{$mode objfpc}{$H+}
uses classes, contnrs;
type
  TItem = class
    N: Integer;
    constructor Create(AN: Integer);
  end;
constructor TItem.Create(AN: Integer); begin N := AN; end;
function ByN(a, b: Pointer): Integer; begin Result := TItem(a).N - TItem(b).N; end;
var l: TFPObjectList; h: TFPHashObjectList; i: Integer;
begin
  l := TFPObjectList.Create(False);
  l.Add(TItem.Create(3)); l.Add(TItem.Create(1)); l.Add(TItem.Create(2));
  l.Sort(@ByN);
  for i := 0 to l.Count - 1 do Write(TItem(l[i]).N, ' ');
  WriteLn(l.IndexOf(l[2]));
  for i := 0 to l.Count - 1 do TItem(l[i]).Free;
  l.Free;
  h := TFPHashObjectList.Create(False);
  h.Add('beta', TItem.Create(20)); h.Add('alpha', TItem.Create(10));
  WriteLn(TItem(h.Find('alpha')).N, ' ', h.Find('gamma') = nil, ' ',
          h.FindIndexOf('beta'), ' ', h.NameOfIndex(1));
  for i := 0 to h.Count - 1 do TItem(h[i]).Free;
  h.Free;
end.
1 2 3 2
10 TRUE 0 alpha

FPC 3.2.2 prints the same.

Warning, on v451: an owning list does not run your destructor. With OwnsObjects true (the default for Create), Delete, Clear, Remove and Free call Free on each item through a TObject, and on v451 a Free through TObject made inside a unit does not reach an overridden Destroy. The list's items are never destroyed: a class that counts its destructor calls counts 0 where FPC counts every item. Until this is fixed, create the list with Create(False) and free each item through its own class type, TItem(l[i]).Free, as the example does. That works in a program and in a unit.

regex: regular expressions for Pascal

The engine behind Nil Python's re, callable from Pascal. ReCompile(pattern, flags) returns a TRegex record, whose ok and error fields say whether the pattern was understood. ReMatch, ReFullMatch, ReSearch and ReSearchFrom return a TReMatch, and ReGroup(m, s, n) gives group n of subject s. ReFindAll, ReReplace and ReQuickMatch complete it. The flags are RE_IGNORECASE, RE_VERBOSE and RE_DOTALL, Python's re.I, re.X and re.S.

program rx;
uses regex;
var r: TRegex; m: TReMatch;
begin
  r := ReCompile('(\w+)-(\d+)', 0);
  m := ReSearch(r, 'see item-42 here');
  WriteLn(m.matched, ' ', ReGroup(m, 'see item-42 here', 1), ' ', ReGroup(m, 'see item-42 here', 2));
  r := ReCompile('foo(?=bar)', 0);
  WriteLn(r.ok, ' ', r.error);
end.
TRUE item 42
FALSE only (?:...) group extensions are supported

The syntax is Python's. Lookahead and lookbehind, backreferences such as \1, named groups (?P<name>...) and possessive quantifiers such as a++ are not supported. ReCompile reports each one through ok and error; check ok before searching. The re entry lists what the same engine does from Python.

random: seeded generators

xoshiro256** seeded through SplitMix64, as a global generator (XoshiroSeed, XoshiroNext, Random64, RandomDouble, RandRange, RandomBytes) and as a state record you own (TRandomState with RandomStateSeed, RandomStateNext, RandomStateRange, RandomStateDouble, RandomStateBytes and RandomStateSplit). OSEntropyBytes and OSEntropy64 read the operating system's generator (getrandom on Linux), and XoshiroRandomize reseeds from hardware entropy, then the operating system. The built-in Random and RandSeed keep working alongside.

program rnd;
uses random;
var st: TRandomState; i, lo, hi, v: Integer; d: Double;
begin
  XoshiroSeed(42);
  for i := 1 to 3 do WriteLn(XoshiroNext);
  RandomStateSeed(st, 42);
  WriteLn(RandomStateNext(st));
  lo := 1000; hi := -1000;
  for i := 1 to 100000 do
  begin
    v := RandomStateRange(st, 1, 6);
    if v < lo then lo := v;
    if v > hi then hi := v;
  end;
  d := RandomStateDouble(st);
  WriteLn(lo, ' ', hi, ' ', (d >= 0) and (d < 1));
end.
1546998764402558742
6990951692964543102
12544586762248559009
1546998764402558742
1 6 TRUE

The first three numbers are those of the published xoshiro256** and SplitMix64 algorithms, written out in Python as the reference. The same program prints the same lines on i386 and aarch64. RandomStateRange(st, lo, hi) includes both ends. A seeded sequence is the same on every run and every target, and none of these generators is fit for keys or tokens: use OSEntropyBytes for those.

base64: encoding bytes as text

One unit for both languages. From Pascal: Base64Encode and Base64Decode over hashing's TByteArray, and Base64EncodeStr and Base64DecodeStr over strings. From Nil Python: base64.b64encode and base64.b64decode.

program b64;
uses sysutils, hashing, base64;
var data: TByteArray;
begin
  WriteLn(Base64EncodeStr('user:pass'));
  WriteLn(Base64EncodeStr('a'), '|', Base64EncodeStr('ab'), '|', Base64EncodeStr('abc'));
  WriteLn(Base64DecodeStr('dXNlcjpw' + #10 + 'YXNz'));
  WriteLn(Base64Decode('dXNl*cjpwYXNz', data));
end.
dXNlcjpwYXNz
YQ==|YWI=|YWJj
user:pass
FALSE
import base64

e = base64.b64encode(b"\x00\xffhello")
print(e, base64.b64decode(e))
print(base64.b64decode("dXNlcjpw\nYXNz"))
b'AP9oZWxsbw==' b'\x00\xffhello'
b'user:pass'

CPython prints the same, and gives the same encodings as the Pascal lines. Line breaks in the input are skipped, as in CPython.

Where it differs from CPython, on v451:

httpjson: JSON over HTTP

HttpGetJson(url, ok) and HttpPostJson(url, body, ok) fetch a URL and parse the reply with the json unit, and JsonParseSafe(text, ok) parses without raising. Each returns nil with ok FALSE when the request fails or the reply is not JSON. Free a result with FreeTree.

The example ran against a local test server on port 8765. GET /info answers {"name": "pxx", "tags": ["a", "b"], "n": 3}, POST /post echoes the body and its Content-Type, and GET /notjson answers the plain text hello.

program hj;
uses sysutils, json, httpjson;
const base = 'http://127.0.0.1:8765';
var v, body, r: TJSONValue; ok: Boolean;
begin
  v := HttpGetJson(base + '/info', ok);
  WriteLn(ok, ' ', v.GetValue('name').AsString, ' ', v.GetValue('tags').Count, ' ', v.GetValue('n').AsInteger);
  v.FreeTree;
  body := JSONParse('{"k": [1, 2]}');
  r := HttpPostJson(base + '/post', body, ok);
  WriteLn(ok, ' ', r.ToString(False));
  r.FreeTree; body.FreeTree;
  v := HttpGetJson(base + '/notjson', ok);
  WriteLn(ok, ' ', v = nil);
  v := HttpGetJson('http://127.0.0.1:1/nothing', ok);
  WriteLn(ok, ' ', v = nil);
  v := JsonParseSafe('{bad', ok);
  WriteLn(ok, ' ', v = nil);
end.
TRUE pxx 2 3
TRUE {"echo":{"k":[1,2]},"ct":"application/json"}
FALSE TRUE
FALSE TRUE
FALSE TRUE

The POST sends Content-Type: application/json. Only plain http:// URLs were tried for this entry. HttpGetJsonAsync and HttpPostJsonAsync exist too; they were not run for this page.

configparser: INI files

Python's configparser, for reading and writing settings files. Sections and options keep the order they were added in, and there is no limit on how many there are.

import configparser

cfg = configparser.ConfigParser()
cfg.add_section("net")
cfg.set("net", "host", "example.org")
cfg.set("net", "port", "8080")
cfg.write("settings.ini")

back = configparser.ConfigParser()
back.read("settings.ini")
print(back.sections())
print(back.get("net", "port"))
print(repr(back.get("net", "missing")))
['net']
8080
''

cfg.write accepts a file name, as above, or an open file, as in CPython (with open("o.ini", "w") as f: cfg.write(f)). For an open file the output is byte for byte what CPython writes.

Where it differs from CPython:

pathlib: paths as objects

The part of Python's pathlib that programs use most: Path(...), joining with /, name, stem, suffix, parent, exists(), is_file(), is_dir(), mkdir(parents=, exist_ok=), open(), read_text(), write_text(), with_suffix() and with_name().

from pathlib import Path

base = Path("out") / "logs"
base.mkdir(parents=True, exist_ok=True)
f = base / "day1.txt"
f.write_text("line one\n")
with f.open("a") as h:
    h.write("line two\n")
print(str(f), f.name, f.stem, f.suffix, str(f.parent))
print(f.exists(), f.is_file(), base.is_dir())
print(str(f.with_suffix(".csv")), str(f.with_name("x.txt")))
out/logs/day1.txt day1.txt day1 .txt out/logs
True True True
out/logs/day1.csv out/logs/x.txt

CPython prints the same three lines.

Limits:

urllib.parse: splitting a URL

urlparse, urlsplit, urlunparse, urlunsplit, quote and unquote, following the rules in CPython's own source.

from urllib.parse import urlparse, urlsplit, quote, unquote

u = urlparse("HTTP://[email protected]:8080/a/b;p=1?q=2&r=3#top")
print(u.scheme, u.netloc, u.path, u.params, u.query, u.fragment)
s = urlsplit("data:text/html;base64,AAA")
print(s.scheme, s.path)
print(quote("a b/c&d"), unquote("a%20b%2Fc"))
http [email protected]:8080 /a/b p=1 q=2&r=3 top
data text/html;base64,AAA
a%20b/c%26d a b/c

CPython prints the same lines. Note the lower-cased scheme, and that a data: URL keeps its ;base64 in the path.

Limits: only these six functions. urlencode, parse_qs, parse_qsl, urljoin and quote_plus are not there, and using one fails to compile (undefined variable (urlencode)).

sqlite3: the DB-API over the system library

import sqlite3 gives a part of Python's DB-API over the system's libsqlite3.so.0: connect, and on the connection execute, executemany, executescript, commit, rollback, close and total_changes. A cursor iterates, unpacks, and answers fetchone and fetchall. Errors raise sqlite3.Error. The program is linked dynamically against SQLite, so the host needs the library installed. To call SQLite's C functions directly instead, see Nil Python: C libraries.

import sqlite3

db = sqlite3.connect("shop.db")
db.execute("CREATE TABLE IF NOT EXISTS item(name TEXT, qty INTEGER, price REAL)")
db.executemany("INSERT INTO item VALUES (?, ?, ?)",
               [("apple", 3, 0.5), ("fig", 2, 1.25), ("pear", 5, 0.4)])
db.commit()
for name, qty, price in db.execute("SELECT name, qty, price FROM item WHERE qty > ? ORDER BY name", (2,)):
    print(name, qty, price)
print(db.execute("SELECT COUNT(*), SUM(qty) FROM item").fetchone())
print(db.total_changes)
try:
    db.execute("SELECT nope FROM item")
except sqlite3.Error as e:
    print("error:", e)
db.close()
apple 3 0.5
pear 5 0.4
[3, 10]
3
error: no such column: nope

Limits:

tempfile: temporary files and directories

NamedTemporaryFile(suffix=, prefix=, dir=, delete=False), mkdtemp(suffix=, prefix=, dir=) and gettempdir().

import tempfile
import os

d = tempfile.mkdtemp(prefix="run-", dir=".")
f = tempfile.NamedTemporaryFile(suffix=".csv", prefix="data-", dir=d, delete=False)
f.close()
print(os.path.isdir(d), oct(os.stat(d).st_mode & 0o777))
print(os.path.basename(f.name).startswith("data-"), f.name.endswith(".csv"), os.path.getsize(f.name))
True 0o700
True True 0

CPython prints the same.

Where it differs from CPython:

re: regular expressions for Nil Python

match, search, fullmatch, findall, sub, subn, split, escape and compile, with the flags re.I, re.X and re.S. A match object has group, start and end. The engine is the regex unit.

import re

m = re.match(r"(\w+)-(\d+)", "item-42 rest")
print(m.group(0), m.group(1), m.group(2), m.start(2))
print(re.findall(r"\d+", "a1 b22 c333"))
print(re.sub(r"(\w+)@(\w+)", r"\2 at \1", "joe@home, ann@work"))
p = re.compile(r"colou?r", re.I)
print(p.findall("Color colour COLOR"))
print(re.split(r"[,;]\s*", "a, b;c"), re.escape("a.b*c"))
print(re.subn(r"o", "0", "foo boo"))
item-42 item 42 5
['1', '22', '333']
home at joe, work at ann
['Color', 'colour', 'COLOR']
['a', 'b', 'c'] a\.b\*c
('f00 b00', 4)

CPython prints the same.

Where it differs from CPython, on v451:

random for Nil Python

seed, random, randint, uniform, choice and shuffle.

import random

random.seed(7)
x = [1, 2, 3, 4]
random.shuffle(x)
print(sorted(x), 1 <= random.randint(1, 6) <= 6, 0 <= random.random() < 1)
print(random.choice("abc") in "abc", 1 <= random.uniform(1, 2) <= 2)
[1, 2, 3, 4] True True
True True

CPython prints the same.

Where it differs from CPython, on v451:

collections: Counter and deque

Counter (with most_common) and deque, imported by name.

from collections import Counter, deque

c = Counter("abracadabra")
print(c.most_common(2))
d = deque([1, 2, 3])
d.appendleft(0)
d.append(4)
print(d.popleft(), d.pop(), len(d))
[('a', 5), ('b', 2)]
0 4 3

CPython prints the same.

Where it differs from CPython, on v451:

html: escaping text for HTML

html.escape(s, quote=True) and html.unescape(s).

import html

s = "<a href=\"x\">Tom & 'Jerry'</a>"
print(html.escape(s))
print(html.escape(s, False))
print(html.unescape("&lt;b&gt; &amp; &quot;q&quot; &#39;s&#39; &#65;&#x42;"))
print(html.unescape("caf&eacute; &copy; &nbsp;x &bogus;"))
&lt;a href=&quot;x&quot;&gt;Tom &amp; &#x27;Jerry&#x27;&lt;/a&gt;
&lt;a href="x"&gt;Tom &amp; 'Jerry'&lt;/a&gt;
<b> & "q" 's' AB
caf&eacute; &copy; &nbsp;x &bogus;

CPython prints the same first three lines. The difference is in the last line: unescape knows the five entities &lt;, &gt;, &amp;, &quot; and &#39; and every numeric form, and leaves other named entities as they are. CPython prints café © followed by a no-break space and x &bogus;.

markdown: README-shaped Markdown to HTML

markdown.markdown(text) renders a subset: ATX headings (#), paragraphs, fenced and indented code, flat unordered and ordered lists, blockquotes, horizontal rules, and the inline forms for code, strong, emphasis, links and bare URLs. The extensions argument is accepted. Of the extensions, nl2br works (a line break becomes <br />) and the unit's source says toc adds heading ids; the rest change nothing, and fenced code renders with or without fenced_code.

import markdown

text = """# Title

Some *emphasis*, **strong** and `code`, with [a link](https://example.org).

- one
- two

Para between.

1. first
2. second

> quoted

```
x = 1
```

---
"""
print(markdown.markdown(text, extensions=["fenced_code"]))
<h1>Title</h1>
<p>Some <em>emphasis</em>, <strong>strong</strong> and <code>code</code>, with <a href="https://example.org">a link</a>.</p>
<ul>
<li>one</li>
<li>two</li>
</ul>
<p>Para between.</p>
<ol>
<li>first</li>
<li>second</li>
</ol>
<blockquote><p>quoted</p></blockquote>
<pre><code>x = 1
</code></pre>
<hr />

Python-Markdown 3.11 gives the same HTML, except that it puts the blockquote on three lines (<blockquote>, <p>quoted</p>, </blockquote>); a browser shows the two the same.

Where it differs from Python-Markdown:

subprocess: running other programs

run, call and Popen with wait and poll, and subprocess.DEVNULL for stdout.

import subprocess

r = subprocess.run(["/bin/sh", "-c", "exit 3"])
print("run:", r.returncode)
print("call:", subprocess.call(["/bin/true"]))
p = subprocess.Popen(["/bin/sh", "-c", "exit 5"])
print("wait:", p.wait())
p = subprocess.Popen(["/bin/sleep", "0.2"], stdout=subprocess.DEVNULL)
print("poll:", p.poll(), p.wait())
run: 3
call: 0
wait: 5
poll: None 0

CPython prints the same.

Where it differs from CPython, on v451:

Next