PONY λ M2 Modula-2

Perl.CodeCompared.To/Python

An interactive executable cheatsheet comparing Perl and Python

Perl 5.40 Python 3.13
Hello World & Running
Hello, World
use v5.38; say "Hello, World!";
print("Hello, World!")
Python needs no use v5.38; preamble and no trailing semicolon — a newline ends a statement, just as it does in Perl once the semicolon is dropped mentally. print() is the direct equivalent of Perl's say: it appends a trailing newline by default. Python has no distinct "modern mode" to opt into; the behavior Perl gates behind use v5.38 is simply how Python always works.
Running a program
# Run directly (no compile step): # perl hello.pl # # Check syntax only: # perl -c hello.pl # # One-liner: # perl -e 'print "Hello!\n"'
# Run directly (no compile step): # python3 hello.py # # One-liner: # python3 -c 'print("Hello!")' # # Interactive REPL: # python3
Both languages are interpreted with no separate compile step, so python3 hello.py and perl hello.pl feel identical to run. Python ships a full interactive REPL (the bare python3 command) for trying expressions one at a time, similar to Perl programmers reaching for a one-liner (perl -e) or the separate Devel::REPL module — Python's REPL is built in and does not require an extra install.
Comments
use v5.38; # Single-line comment — the only inline comment syntax # Perl has no multi-line comment syntax. # Block documentation uses Pod: =begin comment ... =end comment my $name = "Alice"; # inline comment say $name;
# Single-line comment — the only inline comment syntax # Python also has no true multi-line comment syntax. # A triple-quoted string not assigned to anything is sometimes # used as a de facto block comment, but it is really a docstring. name = "Alice" # inline comment print(name)
Both languages use # for single-line comments, and neither has a genuine multi-line comment construct. Perl's workaround is the Pod =begin/=end markers, borrowed from its documentation tooling; Python's closest workaround is an unassigned triple-quoted string, which is really a docstring convention rather than a comment. Consecutive # lines remain the idiomatic style for multi-line commentary in both languages.
print vs say vs print()
use v5.38; print "no newline"; print "\n"; say "with newline"; # say appends a newline automatically say 42; # works with numbers too
print("with newline") # like Perl's say print("no newline", end="") # override the default trailing newline print() # blank line print(42) # works with numbers too, no coercion needed
Python's print() is exactly Perl's say — both append a trailing newline automatically, and both accept a bare number without any explicit stringification. Suppressing the newline in Python requires the end="" keyword argument, the mirror image of Perl's print (no newline) versus say (with newline) being two separate functions.
Variables & Sigils
No sigils — one kind of name
use v5.38; my $name = "Alice"; my @colors = ("red", "green", "blue"); my %person = (name => "Alice", age => 30); say $name; say "@colors"; say $person{name};
name = "Alice" colors = ["red", "green", "blue"] person = {"name": "Alice", "age": 30} print(name) print(", ".join(colors)) print(person["name"])
Python has no sigils at all: a string, a list, and a dict are all referenced with a bare, unpunctuated name — the type is determined by what the variable holds, not by a leading $, @, or %. This eliminates an entire category of Perl syntax rules (element access uses $ even on an @array, interpolation context changes the sigil, and so on): in Python the name never changes no matter how the value is used.
my declaration vs bare assignment
use v5.38; my $count = 0; { my $count = 10; # shadows the outer $count in this block say $count; } say $count;
count = 0 if True: count = 10 # reassigns the SAME variable — no new scope here print(count) print(count)
Perl's my creates a genuinely new lexical variable scoped to the enclosing block, so an inner my $count shadows an outer one. Python has no declaration keyword for local variables — a bare assignment either creates or reassigns a variable in the current scope, and if/while/for blocks do not introduce a new scope the way a Perl { } block does. Only def, class, and lambdas create new scopes in Python.
Dynamic typing in both languages
use v5.38; my $value = 42; say ref(\$value) ? "reference" : "plain scalar"; $value = "hello"; # scalars hold any single value freely $value = [1, 2, 3]; say ref($value); # "ARRAY" — now it's an array reference
value = 42 print(type(value).__name__) # int value = "hello" print(type(value).__name__) # str value = [1, 2, 3] print(type(value).__name__) # list
Both languages let a variable hold a value of any type at any time — neither requires a type declaration. The difference is uniformity: in Perl, a scalar variable can hold a number, a string, or a reference, but an @array variable can never hold a hash without going through a reference. In Python, one bare name can be reassigned freely between an integer, a string, or a list, because every value — even a list or a dict — is just an object referenced by that name.
Constants — convention only
use v5.38; use constant MAX_RETRIES => 3; use constant API_URL => "https://example.com"; say MAX_RETRIES; say API_URL;
MAX_RETRIES = 3 API_URL = "https://example.com" print(MAX_RETRIES) print(API_URL)
Perl's use constant pragma actually enforces immutability — reassigning a Perl constant is a compile error. Python has no constant-declaring syntax whatsoever: SCREAMING_SNAKE_CASE is purely a naming convention that tells other programmers "please do not reassign this," but the interpreter does nothing to stop a later MAX_RETRIES = 5 from silently succeeding. This is a genuine step down in strictness from Perl's pragma-enforced constants.
Truthiness — the "0" trap disappears
use v5.38; # false: undef, 0, "", "0" # true: everything else — including "00" and " " for my $value (undef, 0, "", "0", "00", " ") { my $label = defined($value) ? "'$value'" : "undef"; say "$label is " . ($value ? "truthy" : "falsy"); }
# Python: None, False, 0, 0.0, "", and every EMPTY collection are falsy. # Unlike Perl, the string "0" is truthy in Python — it is simply non-empty. values = [None, 0, "", "0", "00", " ", []] for value in values: label = "None" if value is None else repr(value) print(f"{label} is " + ("truthy" if value else "falsy"))
Python's truthiness diverges from Perl in one sharp, easy-to-miss way: Perl treats the exact string "0" as falsy, but Python does not — any non-empty string is truthy in Python, including "0". Python instead extends falsiness to every empty collection ([], {}, (), set()), which Perl has no unified equivalent for since Perl has no single "empty collection" concept spanning arrays and hashes. A Perl programmer porting a if ($value) check that relies on "0" being falsy will get the wrong answer in Python without adjustment.
Multiple assignment / destructuring
use v5.38; my ($first, $second, $third) = (1, 2, 3); say "$first $second $third"; # Swap without a temp variable ($first, $second) = ($second, $first); say "$first $second"; # Destructure a list my ($head, @tail) = (10, 20, 30, 40); say $head; say "@tail";
first, second, third = 1, 2, 3 print(f"{first} {second} {third}") # Swap without a temporary variable first, second = second, first print(f"{first} {second}") # Destructure a list — the starred name soaks up the rest head, *tail = [10, 20, 30, 40] print(head) print(tail)
Both languages support list-style destructuring assignment and the classic temp-free swap idiom. Python's starred name *tail plays exactly the same role as Perl's @tail soaking up the remaining elements — one of the closer syntactic matches between the two languages — though Python needs no explicit parentheses around the left-hand side and no sigils to distinguish which name captures the rest.
Strings
String interpolation vs f-strings
use v5.38; my $name = "Alice"; my $age = 30; say "Hello, $name! You are $age years old."; say "Next year: @{[ $age + 1 ]}"; # expression interpolation trick
name = "Alice" age = 30 print(f"Hello, {name}! You are {age} years old.") # Any expression works inside {} — no special-casing needed print(f"Next year: {age + 1}")
Perl interpolates a bare $name directly inside a double-quoted string, but embedding an arbitrary expression like age + 1 requires the awkward @{[ expression ]} trick. Python's f-strings (f"...", introduced in 3.6) require an explicit f prefix — ordinary "..." strings do not interpolate at all — but once prefixed, {expression} accepts any expression directly, with no special-casing needed, much like Ruby's #{}.
String methods as method calls
use v5.38; my $text = " Hello, World! "; $text =~ s/^\s+|\s+$//g; # trim: no built-in method, use regex say $text; say length($text); say uc($text); say lc($text); say scalar reverse($text); say index($text, "World") >= 0 ? "found" : "not found";
text = " Hello, World! " print(text.strip()) print(len(text.strip())) print(text.strip().upper()) print(text.strip().lower()) print(text.strip()[::-1]) print("found" if "World" in text else "not found")
Perl's string operations are mostly standalone functions — length($text), uc($text), index($text, ...) — that take the string as an argument, and there is no built-in trim function at all (trimming needs a manual regex substitution). Python's str type has built-in methods called directly on the string object: text.strip(), text.upper(). Reversing, however, has no dedicated method in Python — the idiomatic approach is slice notation [::-1], and substring membership uses the in operator rather than a method or function call.
Heredocs vs triple-quoted strings
use v5.38; my $name = "Alice"; my $message = <<~LETTER; Dear $name, Welcome to the team! Regards, HR LETTER print $message;
name = "Alice" message = f"""Dear {name}, Welcome to the team! Regards, HR """ print(message)
Python has no heredoc syntax; a triple-quoted string ("""...""") is the closest equivalent, and prefixing it with f enables interpolation. Unlike Perl's squiggly heredoc (<<~LABEL), Python's triple-quoted string does not automatically strip common leading indentation — every line is taken completely literally, so the source usually cannot be indented to match the surrounding code without that indentation leaking into the string. The standard library's textwrap.dedent() is the manual workaround when indentation matters.
sprintf-style formatting
use v5.38; printf("%-10s: %6.2f\n", "Price", 9.99); say sprintf("Hex: 0x%08X", 255); say sprintf("Count: %05d", 42);
print("%-10s: %6.2f" % ("Price", 9.99)) print("Hex: 0x%08X" % 255) print("Count: %05d" % 42) # The more modern, preferred way: f-strings with format specs print(f"{'Price':<10}: {9.99:6.2f}") print(f"Hex: 0x{255:08X}")
Python's % operator applies the same printf-style format specifiers Perl's sprintf and printf use — both languages inherited this format-string syntax from C, and the old-style syntax still works identically. Modern Python code prefers f-string format specs ({value:format_spec}) over the % operator, since the format is embedded directly at the interpolation point rather than requiring a separate positional-argument list to keep in sync.
Splitting and joining strings
use v5.38; my $csv = "apple,banana,cherry"; my @fruits = split(/,/, $csv); say "@fruits"; say join(" | ", @fruits); my @words = split ' ', " the quick fox "; # special-cased whitespace split say scalar(@words);
csv = "apple,banana,cherry" fruits = csv.split(",") print(" ".join(fruits)) print(" | ".join(fruits)) # split() with no argument collapses runs of whitespace and drops empty fields words = " the quick fox ".split() print(len(words))
Python's str.split(separator) and separator.join(list) map directly onto Perl's split/join, with the join target and separator swapped: Perl calls join($separator, @list) as a standalone function, while Python calls separator.join(list) as a method on the separator string itself. Python's no-argument str.split() mirrors Perl's special-cased split ' ', $string form exactly — both collapse runs of whitespace and discard leading and trailing empty fields.
Both languages' strings: mutable Perl vs immutable Python
use v5.38; # Perl scalars holding strings are always mutable my $greeting = "hello"; $greeting .= ", world"; # append in place, no error say $greeting;
# Python strings are always immutable — there is no in-place # append operation at all; every "modification" builds a new string greeting = "hello" greeting = greeting + ", world" # rebinds the name to a brand-new string print(greeting) # The += operator works too, but it is still creating a new string, # not mutating the original one in place greeting += "!" print(greeting)
Perl string scalars are always mutable — .= appends in place with no restriction. Python strings are immutable by design and always have been (this is not a new-version toggle the way Ruby 4.0's frozen-by-default strings are) — every operation that looks like mutation, including +=, actually creates a new string object and rebinds the name to it. This has real performance implications for loops that build up a string incrementally; Python's idiomatic fix is "".join(pieces) rather than repeated concatenation.
Numbers
int and float vs Perl's single scalar
use v5.38; my $integer = 42; my $float = 3.14; say ref(\$integer); # "SCALAR" — Perl doesn't distinguish int from float at the type level say $integer + $float; say 2**64; # Perl promotes to floating point beyond native integer range
integer = 42 float_value = 3.14 print(type(integer).__name__) # int print(type(float_value).__name__) # float print(integer + float_value) print(2 ** 64) # exact — arbitrary precision, never promotes to float
Perl has one scalar type for all numbers — integers and floats are the same underlying representation, and very large integers silently become imprecise floating-point approximations once they exceed the native integer range. Python has a real int type with arbitrary precision, just like Ruby: 2 ** 64 is computed exactly, with no float promotion and no precision loss, no matter how large the number grows. float is a genuinely separate type in Python, and mixing int and float in an expression promotes the result to float.
Division and modulo with negative numbers
use v5.38; say 10 / 3; # 3.33333... — Perl's / is always floating-point division say int(10 / 3); # 3 — truncate toward zero for integer division say 10 % 3; # 1 say -7 % 3; # 2 in Perl — result takes the sign of the right operand
print(10 / 3) # 3.3333333333333335 — Python's / is always true division print(10 // 3) # 3 — floor division, a dedicated operator print(10 % 3) # 1 print(-7 % 3) # 2 — same sign convention as Perl (right operand's sign)
Perl's / always performs floating-point division regardless of operand types — getting an integer result requires int(...) to truncate toward zero. Python's / also always performs floating-point ("true") division, but Python additionally has a dedicated floor-division operator, //, which rounds toward negative infinity rather than truncating toward zero. Both languages agree that % returns a result with the same sign as the right operand, so -7 % 3 is 2 in both — this is a genuine similarity, and different from several other languages (including PHP and C) that take the left operand's sign instead.
Built-in functions vs standalone operators
use v5.38; my $number = 42; say $number % 2 == 0 ? "even" : "odd"; say $number > 0 ? "positive" : "not positive"; say abs(-42); for my $count (0..2) { say "tick"; }
number = 42 print("even" if number % 2 == 0 else "odd") print("positive" if number > 0 else "not positive") print(abs(-42)) for count in range(3): print("tick")
Both languages check parity and sign with inline comparison expressions — there is no dedicated even?-style predicate method in either, unlike Ruby. abs() is a built-in free function in Python, matching Perl's standalone abs function exactly rather than Ruby's method-call style. Python's range(3) generates the counting sequence for a fixed number of iterations, the rough equivalent of Perl's 0..2 range in a for loop.
Numeric conversions
use v5.38; my $parsed = "42" + 0; # Perl coerces strings to numbers automatically say $parsed; say "$parsed"; # back to string via interpolation say "3.14" + 0.0; say hex("FF"); # 255 say sprintf("%x", 255); # "ff"
parsed = int("42") print(parsed) print(str(parsed)) print(float("3.14")) print(int("FF", 16)) # hex parsing: 255 print(hex(255)) # to hex string: "0xff" (note the 0x prefix)
Perl coerces strings to numbers implicitly in numeric context — "42" + 0 just works, with warnings if the string is not numeric-looking. Python requires explicit conversion: "42" + 0 raises TypeError, since Python never silently mixes str and int in an operator. int(), float(), and str() are the explicit conversion functions, and int("FF", 16) takes an explicit base argument rather than Perl's dedicated hex() function — Python's own hex() instead goes the other direction, converting a number to a hex string.
Arrays & Lists
Arrays vs lists — a genuine similarity
use v5.38; my @numbers = (1, 2, 3, 4, 5); my @fruits = ("apple", "banana", "cherry"); say $numbers[0]; # element access uses $, not @ say scalar(@fruits); # count say "@fruits"; # interpolates space-joined
numbers = [1, 2, 3, 4, 5] fruits = ["apple", "banana", "cherry"] print(numbers[0]) print(len(fruits)) print(" ".join(fruits))
This is one of the closer conceptual matches in the whole comparison. Perl arrays and Python lists are both heap-allocated, dynamically sized, and freely heterogeneous — a single Python list can mix an int, a str, and another list with no wrapper needed, exactly like a Perl @array can mix a number, a string, and an array reference. The mechanical difference is sigil consistency: Perl declares an array with @numbers but reads a single element with $numbers[0], while Python uses the same bare name numbers everywhere regardless of whether you are addressing the whole list or one element.
Negative indexing and slicing
use v5.38; my @numbers = (10, 20, 30, 40, 50); say $numbers[0]; # first: 10 say $numbers[-1]; # last: 50 say $numbers[-2]; # second-to-last: 40 my @middle = @numbers[1..3]; say "@middle";
numbers = [10, 20, 30, 40, 50] print(numbers[0]) # first: 10 print(numbers[-1]) # last: 50 print(numbers[-2]) # second-to-last: 40 print(numbers[1:4]) # slice: [20, 30, 40] — stop index is EXCLUSIVE
Both languages support negative indexing from the end of the list. Slicing is where they diverge subtly: Perl's @numbers[1..3] range slice is inclusive on both ends, so it includes index 3. Python's numbers[1:4] slice syntax is half-open — the stop index is exclusive — so getting the same three elements (indices 1 through 3) requires writing 4, not 3, as the stop value. This off-by-one difference is a common source of bugs when porting slicing logic directly.
push, pop, shift, unshift
use v5.38; my @items = (2, 3); push @items, 4; # add to end unshift @items, 1; # add to front say "@items"; # 1 2 3 4 my $last = pop @items; # remove from end my $first = shift @items; # remove from front say $last; say $first; say "@items";
items = [2, 3] items.append(4) # add to end — Python's push is called append items.insert(0, 1) # add to front — no dedicated unshift; insert at index 0 print(items) # [1, 2, 3, 4] last_item = items.pop() # remove from end first_item = items.pop(0) # remove from front — pop() takes an optional index print(last_item) print(first_item) print(items)
Python renames Perl's push to append and has no dedicated unshift — inserting at the front uses the general-purpose insert(0, value). Python's pop() is more flexible than Perl's pop/shift pair: called with no argument it removes from the end (like Perl's pop), and called with an explicit index like pop(0) it removes from the front (replacing Perl's shift) or from anywhere else in the list. Both insert(0, ...) and pop(0) are O(n) operations in Python, same as Perl's array-front operations, since both are backed by a contiguous array under the hood.
sort, reverse, and uniq
use v5.38; use List::Util qw(uniq); my @numbers = (3, 1, 4, 1, 5, 9); my @sorted = sort { $a <=> $b } @numbers; say "@sorted"; say join(" ", reverse @sorted); say join(" ", uniq(@numbers));
numbers = [3, 1, 4, 1, 5, 9] print(sorted(numbers)) # numeric sort by default — no comparator needed print(sorted(numbers, reverse=True)) print(list(dict.fromkeys(numbers))) # de-duplicate while preserving first-seen order
Perl's sort defaults to string comparison — sorting numbers correctly requires the explicit { $a <=> $b } comparator block, a common beginner trap. Python's built-in sorted() compares numbers numerically by default, with no comparator needed, and a reverse=True keyword argument replaces a separate reverse call. Python has no built-in uniq: the idiomatic order-preserving de-duplication trick, dict.fromkeys(items), exploits the fact that Python dicts (since 3.7) preserve insertion order and automatically collapse duplicate keys — a Perl List::Util import is not needed at all, but the idiom itself is not obvious without having seen it.
List::Util reduce/sum0 vs built-ins
use v5.38; use List::Util qw(reduce sum0); my @numbers = (1, 2, 3, 4, 5); my $total = sum0(@numbers); my $product = reduce { $a * $b } @numbers; say $total; say $product;
from functools import reduce numbers = [1, 2, 3, 4, 5] total = sum(numbers) product = reduce(lambda accumulator, number: accumulator * number, numbers) print(total) # 15 print(product) # 120
Perl needs the CPAN-distributed core module List::Util to get reduce and sum0 — these are not built into arrays themselves. Python's sum() is a true built-in requiring no import at all, while reduce() — despite being just as fundamental a concept — was deliberately demoted out of the built-ins and into the functools standard-library module in Python 3, on the philosophy that an explicit loop is usually clearer than a general-purpose fold.
Hashes & Dicts
Hashes vs dicts
use v5.38; my %scores = ( alice => 95, bob => 87, carol => 91, ); say $scores{alice}; # element access uses $, not % say scalar(keys %scores); # count
scores = { "alice": 95, "bob": 87, "carol": 91, } print(scores["alice"]) print(len(scores))
Just as with arrays, Perl declares a hash with the % sigil but reads an individual value with $scores{alice}, while Python uses one bare name (scores) for the whole structure and every access into it. Python dict literals use : between key and value where Perl's fat comma => plays the same visual role — and since Python 3.7, a dict's insertion order is guaranteed and preserved, unlike Perl's %hash, whose key order is intentionally unspecified.
Accessing, setting, and checking existence
use v5.38; my %config = (timeout => 30, retries => 3); $config{timeout} = 60; # set / update say $config{timeout}; say exists $config{missing} ? "yes" : "no"; # existence check say $config{missing} // "default"; # defined-or with a default
config = {"timeout": 30, "retries": 3} config["timeout"] = 60 # set / update print(config["timeout"]) print("yes" if "missing" in config else "no") # existence check via 'in' print(config.get("missing", "default")) # get() with a default
Perl's exists checks for key presence without triggering autovivification. Python uses the general-purpose in operator for the same check, the same operator used for substring and list membership — one operator covers what Perl splits across exists, string index, and grep. Python's dict.get(key, default) mirrors Perl's // defined-or default pattern; bracket access on a missing key (config["missing"]) raises KeyError rather than returning undef/None the way Perl silently does.
Iterating over a hash
use v5.38; my %scores = (alice => 95, bob => 87, carol => 91); for my $name (sort keys %scores) { say "$name: $scores{$name}"; } say join(", ", keys %scores); say join(", ", values %scores);
scores = {"alice": 95, "bob": 87, "carol": 91} for name, score in scores.items(): print(f"{name}: {score}") print(", ".join(scores.keys())) print(", ".join(str(value) for value in scores.values()))
Perl's keys and values functions have direct Python method equivalents (.keys(), .values()). Where Perl programmers loop with for my $key (keys %hash) and then look up $hash{$key} inside the loop, Python's .items() yields both the key and value together as a tuple in one step, unpacked directly into name, score — no separate lookup needed, closer to Ruby's each than to Perl's idiom.
Default values and merging
use v5.38; my %word_count; my @words = ("apple", "banana", "apple", "cherry", "apple"); $word_count{$_}++ for @words; # autovivifies each key, starting at undef+1 for my $word (sort keys %word_count) { say "$word: $word_count{$word}"; } my %defaults = (timeout => 30, retries => 3, debug => 0); my %overrides = (timeout => 60); my %merged = (%defaults, %overrides); # later keys win say $merged{timeout};
from collections import defaultdict word_count = defaultdict(int) # default value 0 for missing keys words = ["apple", "banana", "apple", "cherry", "apple"] for word in words: word_count[word] += 1 for word in sorted(word_count): print(f"{word}: {word_count[word]}") defaults = {"timeout": 30, "retries": 3, "debug": False} overrides = {"timeout": 60} merged = defaults | overrides # merge operator (Python 3.9+) — later keys win print(merged["timeout"])
Perl's autovivification lets $word_count{$_}++ spring a missing key into existence and treat undef as 0 for the increment — implicit and occasionally surprising. Python has no autovivification at all; the standard-library collections.defaultdict(int) makes the same behavior explicit by declaring the default-value factory up front, closer in spirit to Ruby's Hash.new(0) than to Perl's implicit magic. The | merge operator (Python 3.9+) is the modern equivalent of Perl's (%defaults, %overrides) list-flattening merge, with later keys winning in both.
Context Sensitivity
Scalar vs list context — a Perl-only concept
use v5.38; my @animals = ("cat", "dog", "bird"); my $count = @animals; # scalar context: array evaluates to its length say $count; # 3 my @copy = @animals; # list context: array evaluates to all its elements say "@copy"; my $last_result = (1, 2, 3); # scalar context on a list: LAST element, not count! say $last_result; # 3 — a classic Perl surprise
animals = ["cat", "dog", "bird"] count = len(animals) # explicit function call — no ambiguity print(count) copy = animals # always the list itself, no context switch print(copy) # There is no equivalent surprise: a list is always a list last_element = [1, 2, 3][-1] print(last_element)
This is the single biggest conceptual difference between the two languages, and Python has no trace of it whatsoever — the same is true when comparing Perl to almost every other mainstream language. In Perl, the same expression (@animals, or a parenthesized list) evaluates differently depending on whether it appears in a context that wants one value (scalar context) or many (list context) — assigning an array to a scalar silently gives you its length, but assigning a literal list (1, 2, 3) to a scalar gives you its last element, not its count. Python expressions always evaluate to exactly one thing, and getting a count always means calling len() explicitly. This eliminates an entire category of Perl surprises, but it also means Python cannot express Perl's occasionally-useful "give me count here, list there" idiom without writing it out explicitly.
wantarray — no Python equivalent
use v5.38; sub context_aware { if (wantarray()) { return (1, 2, 3); # caller wants a list } else { return "scalar result"; # caller wants a scalar } } my @list_result = context_aware(); my $scalar_result = context_aware(); say "@list_result"; say $scalar_result;
# Python functions always return the same value regardless of call site. # There is no equivalent to wantarray — a function cannot inspect how # its return value will be used. def context_aware(): return [1, 2, 3] list_result = context_aware() first_only = context_aware()[0] print(list_result) print(first_only)
Perl's wantarray lets a subroutine inspect the context it was called in and return a different value (or even a different shape of value) depending on whether the caller wants a list, a scalar, or nothing (void context). Python has no equivalent mechanism whatsoever — a function always returns the same value, and it is entirely up to the caller to decide what to do with it (index into it, call len(), or use the whole thing). This is simpler to reason about but means the same expressive trick Perl uses for dual-purpose subroutines must be written as two differently named functions in Python.
Forcing a count — always explicit
use v5.38; my @items = (10, 20, 30, 40); say scalar(@items); # explicit count via the scalar() function say 0 + @items; # numeric context also forces scalar say "There are " . @items . " items"; # string concatenation, too — forces scalar
items = [10, 20, 30, 40] print(len(items)) # always call len() explicitly print(f"There are {len(items)} items") # no implicit context conversion
Perl offers several implicit ways to coerce an array into its count — numeric context, string concatenation, or the explicit scalar() function all work. Python offers none of these shortcuts: attempting to interpolate a list directly into an f-string prints its full contents ([10, 20, 30, 40]), not its length, so len(items) must always be called out explicitly, whether inside a string or anywhere else.
Control Flow
if / elsif / else vs if / elif / else
use v5.38; my $temperature = 22; if ($temperature > 30) { say "hot"; } elsif ($temperature > 20) { say "warm"; } elsif ($temperature > 10) { say "cool"; } else { say "cold"; }
temperature = 22 if temperature > 30: print("hot") elif temperature > 20: print("warm") elif temperature > 10: print("cool") else: print("cold")
Python spells the middle branch elif — a further-abbreviated cousin of Perl's already-shortened elsif. The much bigger adjustment for a Perl programmer is structural, not spelling: Python has no braces at all, and indentation is not just a style convention but the actual mechanism that defines a block's extent — a misindented line is a SyntaxError or, worse, a silent change in program logic, not merely a review comment. Perl's brace-delimited blocks can be indented however the programmer likes with no effect on behavior.
unless and postfix modifiers — a genuine gap
use v5.38; my $authenticated = 0; unless ($authenticated) { say "Please log in"; } # Postfix form say "Please log in" unless $authenticated; say "Welcome!" if $authenticated;
authenticated = False if not authenticated: print("Please log in") # Python has no postfix "if"/"unless" statement modifier and no unless # keyword at all — every guard needs a full if statement: if not authenticated: print("Please log in") if authenticated: print("Welcome!") # The closest Python gets is the conditional EXPRESSION (not a statement # modifier) — useful only when producing a value, not for guard clauses: message = "Welcome!" if authenticated else "Please log in" print(message)
This is a real loss for a Perl programmer: Python has neither an unless keyword nor a postfix statement-modifier form for if at all — Ruby borrowed both of these directly from Perl, but Python never adopted either. Every conditional guard must be written as a full if: block, even for the simplest one-line guard clause that Perl would express as statement unless condition;. Python does have a conditional expression (value_if_true if condition else value_if_false), but it produces a value for assignment rather than gating a statement, so it is not a substitute for the postfix-modifier idiom.
for/foreach vs for ... in
use v5.38; for my $number (1..5) { say $number * $number; } foreach my $letter ("a".."e") { print "$letter "; } print "\n";
for number in range(1, 6): print(number * number) # Python has no native character range — build one from ord()/chr() for code_point in range(ord("a"), ord("f")): print(chr(code_point), end=" ") print()
Perl's numeric range 1..5 corresponds to Python's range(1, 6) — note the stop value must be one past the last number wanted, since range() is exclusive of its stop argument, unlike Perl's inclusive ... Perl's character range "a".."e" has no Python equivalent at all: Python's range() only produces integers, so a character sequence must be built manually via ord() (character to code point) and chr() (code point to character) — a genuine ergonomic gap for a Perl or Ruby programmer used to ranges working uniformly over both.
while loops — no until in Python
use v5.38; my $counter = 0; while ($counter < 5) { say $counter; $counter++; } $counter = 5; until ($counter == 0) { $counter--; } say $counter;
counter = 0 while counter < 5: print(counter) counter += 1 # Python has no "until" keyword — negate the condition instead counter = 5 while counter != 0: counter -= 1 print(counter)
Both languages have a while loop with the same semantics. Python has no until keyword at all (Ruby has one, borrowed from Perl, but Python never adopted it) — "loop while some condition is false" must always be written as a negated while. Python also has no C-style ++/-- increment operators, matching Ruby rather than Perl here: use += 1/-= 1 instead.
given/when workaround vs match statement
use v5.38; my $direction = "north"; for ($direction) { # topicalize $_ — the "poor man's given/when" since Perl deprecated given/when if (/^north$/) { say "Going up"; last; } if (/^south$/) { say "Going down"; last; } if (/^east$/ || /^west$/) { say "Going sideways"; last; } say "Unknown"; }
direction = "north" match direction: case "north": print("Going up") case "south": print("Going down") case "east" | "west": # pipe-separated alternatives print("Going sideways") case _: print("Unknown")
Perl once had an experimental given/when construct but removed it due to design problems with smart matching, leaving Perl programmers to fake it with a for ($topic) { if (...) { ...; last } } loop. Python 3.10's match statement is a clean, permanent replacement, and considerably more powerful than a simple value switch — beyond literal matching it supports full structural pattern matching against sequences, mappings, and class instance attributes in a single case clause, something neither Perl's workaround nor Ruby's case/when reaches without extra library support.
Subroutines & Functions
sub vs def — named parameters vs @_
use v5.38; sub square { my ($number) = @_; # unpack the args array manually return $number * $number; } sub greet { my ($name) = @_; return "Hello, $name!"; } say square(7); say greet("Alice");
def square(number): return number * number def greet(name): return f"Hello, {name}!" print(square(7)) print(greet("Alice"))
Perl subroutines receive all arguments bundled into the single array @_ and must manually unpack them with my ($number) = @_; — there is no way to name parameters in the signature itself in the widely-used, portable syntax. Python functions declare named parameters directly (def square(number):), so there is no unpacking step — a large ergonomic win for a Perl programmer, though Python still requires an explicit return statement (unlike Ruby, which implicitly returns the last expression) since Python has no implicit-return convention at all.
Default parameter values
use v5.38; sub greet { my ($name, $greeting) = @_; $greeting //= "Hello"; # defined-or assigns the default return "$greeting, $name!"; } say greet("Alice"); say greet("Bob", "Hi");
def greet(name, greeting="Hello"): return f"{greeting}, {name}!" print(greet("Alice")) print(greet("Bob", "Hi"))
Perl has no default-parameter syntax in the subroutine signature — the idiomatic approach is unpacking positionally from @_ and then using //= (defined-or assignment) to fill in a default for any argument left undef. Python supports default values directly in the function signature (greeting="Hello"), evaluated once at function-definition time — a well-known Python trap when the default is a mutable object like a list, since every call sharing the un-passed default would share the same list instance.
Simulated vs real keyword arguments
use v5.38; sub create_user { my (%args) = @_; # unpack into a hash to simulate keyword args my $name = $args{name}; my $age = $args{age}; my $city = $args{city} // "Unknown"; say "$name, age $age, from $city"; } create_user(name => "Alice", age => 30, city => "Paris"); create_user(age => 25, name => "Bob"); # order doesn't matter
def create_user(name, age, city="Unknown"): print(f"{name}, age {age}, from {city}") create_user(name="Alice", age=30, city="Paris") create_user(age=25, name="Bob") # order doesn't matter, when called by keyword
Perl simulates keyword arguments by accepting a flat list of key-value pairs and unpacking it into a hash inside the subroutine (my (%args) = @_;) — there is no way for the signature itself to declare which keywords are required or provide defaults. Python has real keyword arguments as a language feature: any parameter can be passed positionally or by name at the call site, required parameters with no default raise TypeError if omitted, and city="Unknown" declares a default directly in the signature — no manual hash unpacking needed.
Variadic arguments — @_ slicing vs *args/**kwargs
use v5.38; sub print_all { my (@items) = @_; say $_ for @items; } sub describe { my (%attributes) = @_; for my $key (sort keys %attributes) { say "$key: $attributes{$key}"; } } print_all(10, 20, 30); describe(name => "Alice", age => 30);
def print_all(*items): for item in items: print(item) def describe(**attributes): for key in sorted(attributes): print(f"{key}: {attributes[key]}") print_all(10, 20, 30) describe(name="Alice", age=30)
Since Perl subroutines already receive every argument in the flat list @_, "variadic" just means unpacking all of it into an array (or a hash, for key-value pairs) rather than a fixed number of named scalars. Python makes the intent explicit in the signature, and reuses almost the exact same star-prefix syntax as Perl's sibling Ruby: *items collects remaining positional arguments into a tuple, and **attributes collects keyword arguments into a dict — both visible directly in the function definition rather than inferred from how @_ happens to be unpacked.
Comprehensions vs map/grep
map/grep chains vs list comprehensions
use v5.38; my @numbers = (1, 2, 3, 4, 5, 6); my @doubled = map { $_ * 2 } @numbers; my @evens = grep { $_ % 2 == 0 } @numbers; my @even_doubled = map { $_ * 2 } grep { $_ % 2 == 0 } @numbers; say "@doubled"; say "@evens"; say "@even_doubled";
numbers = [1, 2, 3, 4, 5, 6] doubled = [number * 2 for number in numbers] evens = [number for number in numbers if number % 2 == 0] even_doubled = [number * 2 for number in numbers if number % 2 == 0] print(doubled) print(evens) print(even_doubled)
This is the concept with no direct Perl equivalent at all — chaining map and grep is Perl's closest approach, but it builds an intermediate list and reads right-to-left (the filter happens before the transform even though map is written first). Python's list comprehension folds a transform and a filter into a single expression, read left-to-right in the natural English order "give me number * 2 for each number in numbers if it is even" — no intermediate list, no block syntax, and it is a first-class expression that can be assigned or returned directly.
Dict and set comprehensions
use v5.38; my @words = ("apple", "banana", "cherry"); # Building a hash from a list requires a manual loop or map returning pairs my %lengths = map { $_ => length($_) } @words; for my $word (sort keys %lengths) { say "$word: $lengths{$word}"; } # "Set" simulated as a hash with the values discarded my %seen = map { $_ => 1 } @words; say join(",", sort keys %seen);
words = ["apple", "banana", "cherry"] # Dict comprehension — builds a dict directly from an expression lengths = {word: len(word) for word in words} for word in sorted(lengths): print(f"{word}: {lengths[word]}") # Set comprehension — a real Set type, not a hash pretending to be one unique_lengths = {len(word) for word in words} print(sorted(unique_lengths))
Perl's closest approach to building a hash from a list is map returning key-value pairs, and Perl has no dedicated set type at all — a "set" is conventionally simulated as a hash whose values are discarded and never inspected. Python has dedicated comprehension syntax for both: a dict comprehension ({key: value for ...}) and a set comprehension ({value for ...}, note the braces with no colon) build the respective real, first-class collection types directly, with automatic de-duplication built into the set comprehension for free.
Generator expressions — lazy evaluation
use v5.38; # Perl has no lazy list-comprehension equivalent — map/grep are always eager, # building the full result list immediately, even if only the first item is needed. my @numbers = (1 .. 1_000_000); my @first_three_evens; for my $number (@numbers) { next unless $number % 2 == 0; push @first_three_evens, $number; last if @first_three_evens == 3; } say "@first_three_evens";
# A generator expression — parentheses instead of square brackets — # produces values lazily, one at a time, with no full list ever built numbers = range(1, 1_000_001) even_numbers = (number for number in numbers if number % 2 == 0) first_three_evens = [] for number in even_numbers: first_three_evens.append(number) if len(first_three_evens) == 3: break print(first_three_evens)
Perl's map/grep are always eager — they build the entire resulting list up front, even when the consuming code only needs the first few elements, so early termination requires falling back to a manual loop with last. Python's generator expression — the same comprehension syntax with parentheses instead of square brackets — evaluates lazily: each value is computed only when the loop asks for it, so iterating a generator built from a million-element range and stopping after three matches does only the minimal work, with no million-element list ever materialized in memory.
References vs Objects
Explicit references vs implicit object references
use v5.38; my @numbers = (1, 2, 3); my $array_ref = \@numbers; # take a reference explicitly with \ say $array_ref; # ARRAY(0x...) — a reference, not the array say $array_ref->[0]; # dereference with -> say @{$array_ref}; # dereference the whole array with @{} say scalar @$array_ref; # shorthand dereference
numbers = [1, 2, 3] same_list = numbers # every name in Python already refers to the OBJECT print(numbers) # [1, 2, 3] — printing a list shows its contents directly print(numbers[0]) # no special dereference operator needed same_list.append(4) # mutating through either name affects the same list print(numbers) # [1, 2, 3, 4] — numbers sees the change too
This is one of the friendliest surprises for a Perl programmer learning Python: Python's object model is much closer to Perl's references than to Perl's plain scalars. In Perl, a plain @numbers is a value, and getting reference semantics (needed to nest a list inside a hash, or to have two variables share the same underlying data) requires an explicit \ to take a reference and -> or @{} to dereference it. In Python, every name is already a reference to an object — assigning same_list = numbers does not copy the list, it makes two names point at the same list, exactly like Perl's my $array_ref = \@numbers;, but with none of Perl's explicit sigil-based dereference ceremony.
Nested structures — no reference needed to nest
use v5.38; # Perl arrays/hashes cannot directly contain other arrays/hashes — # nesting always goes through a reference my %company = ( name => "Acme", employees => [ { name => "Alice", age => 30 }, { name => "Bob", age => 25 }, ], ); say $company{employees}[0]{name}; # arrow between elements is optional here
# Python lists and dicts nest directly — no reference syntax needed at all company = { "name": "Acme", "employees": [ {"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}, ], } print(company["employees"][0]["name"])
Because Perl arrays and hashes are plain values by default, nesting one inside another requires going through a reference — employees => [ { ... }, { ... } ] is really an array reference containing hash references, and Perl's syntax quietly allows dropping the -> arrows between consecutive subscripts as a convenience. Python needs no reference syntax to nest at all, because every list and dict is already reference-like: company["employees"][0]["name"] chains subscripts directly with no arrows, ever, since there is no distinction between "the value" and "a reference to the value" for Python to paper over.
Regular Expressions
Native =~ operator vs the re module
use v5.38; my $text = "The year is 2026"; if ($text =~ /(\d+)/) { say "Found: $1"; } say "matches" if $text =~ /year/; (my $redacted = $text) =~ s/\d+/XXXX/; say $redacted;
import re text = "The year is 2026" match = re.search(r"(\d+)", text) if match: print(f"Found: {match.group(1)}") print("matches" if re.search(r"year", text) else "no match") redacted = re.sub(r"\d+", "XXXX", text) print(redacted)
This is a real ergonomic step down for a Perl programmer, even though the regex pattern language itself is nearly identical (Python's re syntax is heavily derived from Perl's). Perl bakes regex matching directly into the language as the =~ binding operator, with automatic capture-group variables ($1, $2) populated as a side effect of a successful match. Python has no regex operator at all — matching is always an explicit function call into the re standard-library module, which must be imported, and a successful match returns a Match object that capture groups must be pulled out of explicitly with .group(1) rather than appearing automatically in numbered variables.
Named capture groups
use v5.38; my $date = "2026-07-03"; if ($date =~ /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/) { say "Year: $+{year}, Month: $+{month}, Day: $+{day}"; }
import re date = "2026-07-03" match = re.search(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})", date) if match: print(f"Year: {match.group('year')}, Month: {match.group('month')}, Day: {match.group('day')}") print(match.groupdict()) # all named groups as a dict at once
Both languages support named capture groups with nearly identical syntax — Perl's (?<name>...) versus Python's (?P<name>...), the small P being the only difference, a strong sign of Python re's Perl heritage. Perl exposes named captures through the special %+ hash; Python exposes them through match.group("name") for one at a time, or match.groupdict() to get every named group back as a single dict in one call — no equivalent single-call convenience exists on the Perl side.
Global matching — g flag vs findall/finditer
use v5.38; my $text = "cat bat hat mat"; my @matches = ($text =~ /(\w)at/g); say "@matches"; # c b h m — all captured groups, flattened my $count = () = $text =~ /at/g; # the classic "goatse" idiom for counting matches say $count;
import re text = "cat bat hat mat" matches = re.findall(r"(\w)at", text) print(matches) # ['c', 'b', 'h', 'm'] — a real list, not a flattened string count = len(re.findall(r"at", text)) print(count)
Perl's /g flag in list context returns every match (or every captured group, if the pattern has one), and counting matches relies on the famously cryptic "goatse" idiom, my $count = () = $text =~ /at/g;, which assigns into an empty list purely to force list context and then re-assigns the resulting count into a scalar. Python's re.findall() returns an ordinary list of matches directly with no context tricks needed, and re.finditer() is the lazy, generator-based sibling for very large inputs — counting is simply len(re.findall(...)), with nothing to explain about why it works.
OOP
bless-based objects vs native class syntax
use v5.38; package Dog; sub new { my ($class, %args) = @_; my $self = { name => $args{name}, breed => $args{breed} }; return bless $self, $class; } sub bark { my ($self) = @_; return "$self->{name} says Woof!"; } package main; my $dog = Dog->new(name => "Rex", breed => "Labrador"); say $dog->bark;
class Dog: def __init__(self, name, breed): self.name = name self.breed = breed def bark(self): return f"{self.name} says Woof!" dog = Dog("Rex", "Labrador") print(dog.bark())
This is one of Python's biggest ergonomic wins over Perl. Perl has no native class syntax at all — an "object" is conventionally just a hash reference that has been blessed into a package, associating it with that package's namespace for method lookup, and every method must manually unpack $self as the first element of @_. Python has a real class keyword, and self is an explicit but ordinary first parameter Python passes automatically at the call site — a genuine language construct rather than a manual convention built out of hash references and blessing.
@ISA and parent vs class Subclass(Parent)
use v5.38; package Animal; sub new { my ($class, %args) = @_; return bless { name => $args{name} }, $class; } sub speak { my ($self) = @_; return "$self->{name} makes a sound"; } package Dog; use parent -norequire, "Animal"; sub speak { my ($self) = @_; my $base = $self->SUPER::speak(); return "$base: Woof!"; } package main; my $dog = Dog->new(name => "Rex"); say $dog->speak;
class Animal: def __init__(self, name): self.name = name def speak(self): return f"{self.name} makes a sound" class Dog(Animal): def speak(self): base = super().speak() return f"{base}: Woof!" dog = Dog("Rex") print(dog.speak())
Perl expresses inheritance through the package-level @ISA array (set idiomatically via the parent or base pragma) and calls the parent's overridden method with the special SUPER:: pseudo-package. Python declares a base class directly in the class header (class Dog(Animal):) and calls the overridden method with the built-in super() function — no separate pragma or package-variable manipulation needed, and Python additionally supports genuine multiple inheritance with a well-defined method resolution order, which Perl's @ISA mechanism handles far less predictably.
Overloading vs dunder methods
use v5.38; use overload '+' => \&add, '""' => \&stringify; package Point; sub new { my ($class, $x, $y) = @_; return bless { x => $x, y => $y }, $class; } sub add { my ($self, $other) = @_; return Point->new($self->{x} + $other->{x}, $self->{y} + $other->{y}); } sub stringify { my ($self) = @_; return "($self->{x}, $self->{y})"; } package main; my $point_one = Point->new(1, 2); my $point_two = Point->new(3, 4); say $point_one->add($point_two)->stringify;
class Point: def __init__(self, x, y): self.x = x self.y = y def __add__(self, other): return Point(self.x + other.x, self.y + other.y) def __repr__(self): return f"({self.x}, {self.y})" point_one = Point(1, 2) point_two = Point(3, 4) print(point_one + point_two) # uses __add__, then __repr__ to print the result
Perl's use overload pragma maps operators to handler subroutines using string keys like '+' and '""' (for stringification) — a somewhat indirect, string-based mapping declared once per package. Python's equivalent, "dunder" (double-underscore) methods — __add__ for +, __repr__ for the developer-facing string representation, __str__ for the user-facing one, and dozens more — are ordinary named methods defined directly on the class, with no separate registration pragma, and point_one + point_two works automatically the moment __add__ exists.
Manual accessors vs @dataclass
use v5.38; package Point; sub new { my ($class, %args) = @_; return bless { x => $args{x}, y => $args{y} }, $class; } sub x { my ($self) = @_; return $self->{x}; } sub y { my ($self) = @_; return $self->{y}; } sub to_string { my ($self) = @_; return "Point(" . $self->x . ", " . $self->y . ")"; } package main; my $point = Point->new(x => 3, y => 4); say $point->to_string;
from dataclasses import dataclass @dataclass class Point: x: int y: int point = Point(x=3, y=4) print(point) # __repr__, __init__, and __eq__ generated automatically print(point.x, point.y) # attribute access with no manual accessor methods
Writing a plain data-holding object in Perl means hand-writing a constructor and an accessor method for every field, with no built-in shortcut — CPAN modules like Moo or Class::Accessor exist specifically to remove this boilerplate. Python's @dataclass decorator (standard library, no install needed) generates the constructor, a readable __repr__, and value-based equality (__eq__) automatically from a short list of type-annotated field declarations — attributes are accessed directly as point.x, with no manual getter method to write.
Decorators
Decorators — no native Perl equivalent
use v5.38; use Time::HiRes qw(time); # Perl has no decorator syntax — the closest approach is a manual # wrapper subroutine that you call explicitly instead of the original sub timed { my ($function) = @_; return sub { my $start = time(); my $result = $function->(@_); my $elapsed = time() - $start; printf("Elapsed: %.4fs\n", $elapsed); return $result; }; } my $slow_square = timed(sub { my ($number) = @_; return $number * $number; }); say $slow_square->(5);
import time def timed(function): def wrapper(*args, **kwargs): start = time.time() result = function(*args, **kwargs) elapsed = time.time() - start print(f"Elapsed: {elapsed:.4f}s") return result return wrapper @timed def slow_square(number): return number * number print(slow_square(5))
Perl has nothing resembling decorator syntax: wrapping a subroutine to add cross-cutting behavior (timing, logging, memoization, access control) means manually writing a higher-order subroutine that takes a code reference and returns a new one, then explicitly calling the wrapped version everywhere the original would have been called. Python's @decorator syntax is pure sugar over exactly that same higher-order-function pattern, but applied once at the definition site — @timed above def slow_square rebinds the name automatically, so every future call to slow_square(...) is already wrapped, with no separate wrapped-variable name to remember to use.
Built-in decorators: @staticmethod, @property
use v5.38; package Circle; sub new { my ($class, $radius) = @_; return bless { radius => $radius }, $class; } # Perl has no built-in "computed attribute" syntax — this is just a # regular method that happens to compute its result each call sub area { my ($self) = @_; return 3.14159265 * $self->{radius} ** 2; } # Perl has no static-method distinction — a "class method" is simply # a sub that ignores $self / uses $class instead sub unit_circle { my ($class) = @_; return $class->new(1); } package main; my $circle = Circle->new(2); say $circle->area; say Circle->unit_circle->area;
import math class Circle: def __init__(self, radius): self.radius = radius @property def area(self): return math.pi * self.radius ** 2 @staticmethod def unit_circle(): return Circle(1) circle = Circle(2) print(circle.area) # no parentheses — looks like plain attribute access print(Circle.unit_circle().area)
Perl has no dedicated syntax to distinguish a "computed attribute" from an ordinary method, or a "class-level" method from an instance method — both are just conventions applied to a plain sub. Python ships several built-in decorators that make these distinctions explicit and change calling syntax: @property turns a method into something accessed like a plain attribute with no parentheses (circle.area, not circle.area()), and @staticmethod marks a method that receives no implicit self at all, called directly on the class.
Error Handling
eval/$@ vs try/except
use v5.38; my $result = eval { die "Something went wrong!\n"; return 42; # never reached }; if ($@) { say "Caught error: $@" =~ s/\n$//r; } else { say "Result: $result"; }
try: raise Exception("Something went wrong!") result = 42 # never reached except Exception as error: print(f"Caught error: {error}") else: print(f"Result: {result}")
Perl's traditional error handling wraps risky code in an eval { } block and checks the special global variable $@ afterward for a truthy (non-empty) value — an easy-to-miss check, since forgetting to look at $@ silently swallows the error. Python's try/except is structurally explicit: an exception either propagates past an except that does not match its type, or is caught and bound to a named variable (as error) directly in the clause that handles it, with no separate global-variable check required, and an else clause runs only when no exception occurred at all.
Exception objects vs custom exception classes
use v5.38; package InsufficientFundsError; sub new { my ($class, $message) = @_; return bless { message => $message }, $class; } sub message { my ($self) = @_; return $self->{message}; } package main; sub withdraw { my ($balance, $amount) = @_; if ($amount > $balance) { die InsufficientFundsError->new("Cannot withdraw $amount from $balance"); } return $balance - $amount; } eval { withdraw(100, 150); }; if (my $error = $@) { if (ref($error) && $error->isa("InsufficientFundsError")) { say "Error: " . $error->message; } }
class InsufficientFundsError(Exception): pass def withdraw(balance, amount): if amount > balance: raise InsufficientFundsError(f"Cannot withdraw {amount} from {balance}") return balance - amount try: withdraw(100, 150) except InsufficientFundsError as error: print(f"Error: {error}")
Perl can die with any blessed object, and catching a specific error type means manually checking ref($error) and calling ->isa(...) inside the eval/$@ pattern. Python's exception system is built around a real class hierarchy rooted at Exception: defining a custom exception is as simple as subclassing it (often with an empty body, as here, inheriting all the useful behavior), and except InsufficientFundsError as error: matches only that exception type (and its subclasses) automatically, with no manual type check needed inside the handler.
local $@ cleanup vs finally
use v5.38; sub process_resource { say "Acquiring resource"; eval { die "Processing failed!\n"; }; my $error = $@; say "Releasing resource"; # manual cleanup, must not be forgotten die $error if $error; } eval { process_resource(); }; say "Caught: $@" =~ s/\n$//r if $@;
def process_resource(): print("Acquiring resource") try: raise Exception("Processing failed!") finally: print("Releasing resource") # ALWAYS runs, even if an exception propagates try: process_resource() except Exception as error: print(f"Caught: {error}")
Perl has no dedicated cleanup-guaranteed block — ensuring code runs whether or not an error occurred means manually saving $@, running the cleanup code, and re-die-ing the saved error afterward, an easy step to get wrong or forget. Python's finally clause guarantees its body runs no matter what happens in the try block — whether it completes normally, raises a caught exception, or raises an exception that propagates all the way out — with no manual error-variable bookkeeping required.
File I/O
open/close vs the with statement
use v5.38; # This example cannot run in the sandboxed Compiler Explorer environment — # it demonstrates the API shape rather than executing against a real file. open(my $filehandle, '>', 'output.txt') or die "Cannot open: $!"; print $filehandle "Hello, file!\n"; close($filehandle); open(my $read_handle, '<', 'output.txt') or die "Cannot open: $!"; while (my $line = <$read_handle>) { print $line; } close($read_handle);
# This example cannot run in the sandboxed PyScript/Pyodide environment — # it demonstrates the API shape rather than executing against a real file. with open("output.txt", "w") as file: file.write("Hello, file!\n") # The file is automatically closed here, even if an exception was raised — # no separate close() call, and no risk of forgetting it with open("output.txt", "r") as file: for line in file: print(line, end="")
Perl's file handling requires an explicit close() call (or relies on the filehandle going out of scope and being cleaned up by garbage collection, which is not guaranteed to happen promptly) — forgetting it is a classic resource leak. Python's with open(...) as file: context manager guarantees the file is closed the moment the block exits, whether normally or via an exception, with no separate close call to remember; this example cannot run in this browser sandbox's in-memory execution environment, which has no real filesystem, so it is shown for the API shape rather than live execution.
Reading lines — chomp vs automatic stripping
use v5.38; # API-shape example — cannot execute against a real file in this sandbox. open(my $filehandle, '<', 'data.txt') or die "Cannot open: $!"; my @lines; while (my $line = <$filehandle>) { chomp $line; # strip the trailing newline manually push @lines, $line; } close($filehandle); say "@lines";
# API-shape example — cannot execute against a real file in this sandbox. with open("data.txt", "r") as file: lines = [line.rstrip("\n") for line in file] print(lines) # readlines() reads them all into a list at once; splitlines() on a # whole-file string is another common alternative to strip newlines with open("data.txt", "r") as file: content = file.read() lines_alt = content.splitlines() # splits AND strips newlines in one call print(lines_alt)
Perl's line-reading loop yields each line with its trailing newline still attached, so chomp must be called explicitly to strip it — a step that is easy to forget and produces subtly wrong string comparisons when skipped. Python's file iteration has the same behavior (each line keeps its newline), stripped here with rstrip("\n"), but the standard library also offers str.splitlines(), which splits a whole string into lines and discards the newlines in a single call — a convenience with no single-call Perl equivalent.
Special Variables
$_ topic variable — no Python equivalent
use v5.38; for (1..3) { say; # implicitly prints $_ say "Value: $_"; } my @words = ("cat", "dog", "bird"); say for @words; # $_ set automatically each iteration
# Python has no implicit topic variable at all — every loop variable # must be named explicitly, even when the name would just be "it" or "value" for number in range(1, 4): print(number) print(f"Value: {number}") words = ["cat", "dog", "bird"] for word in words: print(word)
Perl's $_ is a genuinely distinctive feature: many built-in functions (say, print, chomp, pattern matching) default to operating on $_ when called with no argument, and loops set it implicitly, enabling extremely terse one-liners like say for @words;. Python has no topic variable and no functions that implicitly default to one — every loop variable must always be given an explicit name, and every function call must always be given its argument explicitly. This is more verbose but removes an entire class of "wait, which value is $_ right now?" bugs that can arise when Perl code nests loops or calls without renaming $_.
@ARGV and %ENV vs sys.argv and os.environ
use v5.38; # API-shape example — no real command-line args or environment in this sandbox. say "Program name: $0"; say "Number of args: " . scalar(@ARGV); say "First arg: $ARGV[0]" if @ARGV; say "Home directory: $ENV{HOME}";
# API-shape example — no real command-line args or environment in this sandbox. import sys import os print(f"Program name: {sys.argv[0]}") print(f"Number of args: {len(sys.argv) - 1}") # argv[0] is the script name itself if len(sys.argv) > 1: print(f"First arg: {sys.argv[1]}") print(f"Home directory: {os.environ['HOME']}")
Perl's @ARGV holds only the actual command-line arguments (the script name lives separately in $0), while Python's sys.argv is a single list that includes the script name itself as element 0 — a common off-by-one trap when porting argument-counting logic directly. Perl's %ENV hash and Python's os.environ mapping serve the identical purpose of exposing environment variables, with near-identical bracket-access syntax once the import os is in place.
CPAN vs PyPI
CPAN vs PyPI and pip
use v5.38; # Installing a package (run outside this sandbox): # cpan JSON::PP # cpanm JSON::PP # cpanminus — the more common modern installer # # Using it: use JSON::PP qw(encode_json decode_json); my $json_text = encode_json({ name => "Alice", age => 30 }); say $json_text;
# Installing a package (run outside this sandbox): # pip install requests # python3 -m pip install requests # equivalent, explicit form # # Using it — json is part of Python's standard library, no install needed: import json json_text = json.dumps({"name": "Alice", "age": 30}) print(json_text)
CPAN (the Comprehensive Perl Archive Network) predates PyPI (the Python Package Index) by roughly a decade and pioneered the idea of a language-wide package repository with mandatory documentation and test coverage standards — a high bar PyPI does not enforce as strictly. cpanm (cpanminus) and pip serve the same installation role for their respective ecosystems. One genuine Python advantage here: json, alongside many other common utility modules, ships in Python's standard library, whereas Perl's equivalent (JSON::PP or the faster JSON::XS) is a separate CPAN install — Python's "batteries included" philosophy covers more everyday needs without leaving the standard library at all.
local::lib/carton vs virtual environments
use v5.38; # Perl dependency isolation — run outside this sandbox: # # cpanm --local-lib=local_dir DependencyName # local::lib style # # # Or with a lockfile via Carton (Perl's rough Bundler/pip-tools analog): # cpanm Carton # carton install # carton exec -- perl script.pl
# Python dependency isolation — run outside this sandbox: # # python3 -m venv .venv # create an isolated environment # source .venv/bin/activate # activate it (per-shell) # pip install requests # installs into .venv, not system-wide # pip freeze > requirements.txt # lock the installed versions # # deactivate # leave the virtual environment
Perl's dependency isolation is opt-in and less standardized: local::lib redirects installs to a project-local directory, and Carton (modeled explicitly on Ruby's Bundler) adds a lockfile on top of that for reproducible installs — but neither is bundled with Perl itself, and plenty of Perl projects still install dependencies system-wide. Python's venv module ships in the standard library and is the near-universal convention: every serious Python project is expected to use a virtual environment, and installing a package without one active is considered a mistake in most teams' workflows, not merely an option.