# Python to Perl You know Python. You write list comprehensions in your sleep, you reach for a `dict` without thinking, and `for x in xs:` is muscle memory. This guide maps each of those habits onto Perl, so you can be productive in `pperl` in an afternoon instead of relearning programming from scratch. Two shocks land in the first five minutes, so meet them now: - **Perl uses braces and semicolons, not indentation.** There is no significant whitespace. A block is `{ ... }`; a statement ends with `;`. Indent for your own sanity, but the parser ignores it. - **Variables wear sigils.** A scalar is `$x`, an array is `@x`, a hash is `%x`. The sigil is part of how you read the variable, not just its name. This is the single biggest surface difference from Python, and section one is about nothing else. Every example here runs on `pperl`, a Rust reimplementation of Perl 5.42. Each Perl snippet was executed on `pperl`; the `# ...` comments show its real output. Start your scripts with: ```perl use v5.42; ``` That one line turns on `strict` (no accidental globals), `warnings`, the `say` built-in (a `print` with a trailing newline, like Python's `print`), subroutine signatures, and the modern feature set used throughout this guide. ## How to read this - [Variables and sigils](#variables-and-sigils) - the `$`/`@`/`%` shift. - [Strings](#strings) - interpolation and concatenation. - [Numbers](#numbers) - one surface type, float division. - [Arrays and lists](#arrays-and-lists) - your `list`, plus `map`/`grep`. - [Hashes](#hashes) - your `dict`. - [Context](#context) - the concept Python has no word for. Read this one. - [Truthiness](#truthiness) - what counts as false, with surprises. - [Control flow](#control-flow) - `if`, loops, the postfix forms. - [Subroutines](#subroutines) - `def` becomes `sub`. - [References](#references) - nested data and closures. - [Regular expressions](#regular-expressions) - regex as syntax, not a library. - [File and stream I/O](#file-and-stream-io) - `open`, line reads, slurp. - [Modules and packages](#modules-and-packages) - `import` becomes `use`. - [Objects](#objects) - the modern `class` syntax. - [Error handling](#error-handling) - `try`/`except` becomes `eval` or `try`/`catch`. - [Idioms](#idioms) - the daily one-liners. - [Gotchas coming from Python](#gotchas-coming-from-python) - scan before you write. ## Variables and sigils In Python a name just names a value. In Perl the **sigil tells you the shape**: `$` for a single value, `@` for an ordered list, `%` for a key/value map. `my` declares a lexically scoped variable, the Perl equivalent of an ordinary local binding. ```python name = "Ada" langs = ["Python", "Perl"] ages = {"Ada": 36, "Linus": 56} ``` ```perl use v5.42; my $name = "Ada"; my @langs = ("Python", "Perl"); my %ages = (Ada => 36, Linus => 56); say $name; # Ada say $langs[0]; # Python say $ages{Ada}; # 36 ``` The gotcha: when you reach *into* an aggregate for one element, the sigil follows the value you get back, not the container. A single element of `@langs` is a scalar, so it is `$langs[0]`, not `@langs[0]`; one value from `%ages` is `$ages{Ada}`. The `@`/`%` form is the whole collection; the `$` form is one piece of it. ## Strings Double quotes interpolate variables, single quotes do not. There is no f-string because the default string *is* an f-string. Concatenation is `.`, not `+` (which is reserved for numbers). ```python name = "Ada" greeting = f"Hello, {name}" shout = "AB" * 3 ``` ```perl use v5.42; my $name = "Ada"; my $greeting = "Hello, $name"; # Hello, Ada my $literal = 'Hello, $name'; # Hello, $name (no interpolation) my $joined = "AB" . "CD"; # ABCD my $shout = "AB" x 3; # ABABAB ``` The gotcha: string repetition is `x`, not `*`, and the join operator is `.`, not `+`. Mixing them up is the most common first-day typo for a Python developer. Array elements and hash values interpolate inside double quotes too: `"First: $langs[0]"` works as written. ## Numbers Perl has one numeric type at the surface; you do not declare `int` vs `float`. Division always produces a float, exactly like Python 3. ```python print(7 / 2) # 3.5 print(7 // 2) # 3 print(2 ** 10) # 1024 print(10 % 3) # 1 ``` ```perl use v5.42; say 7 / 2; # 3.5 say int(7 / 2); # 3 say 2 ** 10; # 1024 say 10 % 3; # 1 ``` The gotcha: comparison and equality operators come in two families, and you pick by the *operands' type*, not the variables'. Numbers use `==`, `!=`, `<`, `>`, `<=>`; strings use `eq`, `ne`, `lt`, `gt`, `cmp`. `"10" == "10.0"` is true (numeric), but `"10" eq "10.0"` is false (string). Using `==` on strings, or `eq` on numbers, is a frequent and quiet bug. ## Arrays and lists A Perl array is your Python `list`. The verbs are renamed but the operations are the same, and negative indices work as you expect. ```python nums = [1, 2, 3, 4, 5] nums.append(6) last = nums.pop() print(len(nums)) # 5 print(nums[1:3]) # [2, 3] ``` ```perl use v5.42; my @nums = (1, 2, 3, 4, 5); push @nums, 6; my $last = pop @nums; # 6 say scalar @nums; # 5 say "@nums[1,2]"; # 2 3 (a slice) ``` Comprehensions become `map` and `grep`. `map` transforms (Python's `[f(x) for x in xs]`); `grep` filters (Python's `[x for x in xs if p(x)]`). `$_` is the current element, Perl's implicit loop variable. ```python squares = [x * x for x in nums] evens = [x for x in nums if x % 2 == 0] desc = sorted(nums, reverse=True) ``` ```perl use v5.42; my @nums = (1, 2, 3, 4, 5); my @squares = map { $_ * $_ } @nums; # 1 4 9 16 25 my @evens = grep { $_ % 2 == 0 } @nums; # 2 4 my @desc = sort { $b <=> $a } @nums; # 5 4 3 2 1 ``` The gotcha: `sort` compares as strings by default, so `sort (10, 9, 100)` gives `10 100 9`. For numbers you must pass a comparator: `sort { $a <=> $b } @nums`. `$a` and `$b` are the two elements being compared, and `<=>` is the numeric three-way operator (`cmp` is its string sibling). ## Hashes A Perl hash is your Python `dict`. Declared with `%`, accessed one value at a time with `$h{key}`. ```python h = {"apple": 3, "pear": 5, "plum": 2} print(h["apple"]) # 3 print(sorted(h.keys())) # ['apple', 'pear', 'plum'] print("pear" in h) # True del h["plum"] for k in sorted(h): print(k, "=>", h[k]) ``` ```perl use v5.42; my %h = (apple => 3, pear => 5, plum => 2); say $h{apple}; # 3 say join ",", sort keys %h; # apple,pear,plum say exists $h{pear} ? "yes" : "no"; # yes delete $h{plum}; for my $k (sort keys %h) { say "$k => $h{$k}"; } ``` The gotcha: membership is `exists $h{key}`, not Python's `in`, and the fat comma `=>` is the idiomatic key/value separator. `=>` also auto-quotes the bareword on its left, so `apple => 3` needs no quotes around `apple`. A hash has no order; iterate `sort keys %h` when you want a stable one. ## Context This is the chapter to read twice. Python has no equivalent, and almost every "Perl is weird" story traces back to it. Every expression in Perl runs in either **list context** or **scalar context**, decided by where it appears, not by what it is. The same array behaves differently in each. Assigning an array to a *scalar* gives its length; assigning it to a *list* (a parenthesised set of variables) copies elements. ```perl use v5.42; my @list = (10, 20, 30); my $count = @list; # scalar context -> 3 (the length) say $count; # 3 my ($first) = @list; # list context -> first element copied say $first; # 10 my ($a, $b) = @list; # 10 and 20 say "$a $b"; # 10 20 ``` The parentheses on the left are the whole difference. `my $x = @list` asks "how many?" and gets `3`. `my ($x) = @list` asks "give me the elements" and gets `10`. A Python developer reads both as "assign the list," which is exactly the trap. A subroutine can ask which context it was called in, with `wantarray`, and return different things accordingly: ```perl use v5.42; sub items { return wantarray ? (1, 2, 3) : "three items"; } my @got = items(); # list context my $msg = items(); # scalar context say "@got"; # 1 2 3 say $msg; # three items ``` Built-ins use this too. `scalar @array` forces scalar context to get a count; a regex match with `/g` returns the list of matches in list context and a true/false in scalar context. The classic idiom for "count the matches" forces a list assignment in scalar context with the so-called *countof* form: ```perl use v5.42; my $n = () = "a1b2c3" =~ /\d/g; say $n; # 3 ``` The gotcha: there is no single answer to "what does this array do" - it depends on the surrounding expression. When something returns a number you expected to be a list (or vice versa), context is the first suspect. Reach for `scalar` to force a count and parentheses on the left to force element copying. ## Truthiness Perl's false values are a short, specific list: the number `0`, the empty string `""`, the string `"0"`, and `undef` (Perl's `None`). **Everything else is true** - and that includes some strings a Python developer assumes are false. ```perl use v5.42; for my $v (0, 1, "", "0", "00", "0.0", "false") { say "'$v' is ", ($v ? "true" : "false"); } ``` This prints, exactly: ```text '0' is false '1' is true '' is false '0' is false '00' is true '0.0' is true 'false' is true ``` The gotcha: `"00"` and `"0.0"` are **true** in Perl, even though they look numerically zero, because only the exact string `"0"` is false. The string `"false"` is true (it is a non-empty, non-`"0"` string). And unlike Python, an empty array or empty hash is not itself a false *value* - in boolean context an array yields its length, so `if (@list)` is true when the list is non-empty, which is the test you want. For a "use this if defined, else a default" choice, use defined-or `//`, which falls back only on `undef` (not on `0` or `""`): ```perl use v5.42; my %cfg = (host => "localhost"); my $port = $cfg{port} // 8080; # key absent -> undef -> 8080 say $port; # 8080 ``` ## Control flow The keywords are familiar; the spelling differs. `elif` is `elsif`, and there is no `:` and no indentation - blocks are braced. ```python if n < 0: sign = "neg" elif n == 0: sign = "zero" else: sign = "pos" for x in nums: print(x) while running: step() ``` ```perl use v5.42; my $sign; if ($n < 0) { $sign = "neg" } elsif ($n == 0) { $sign = "zero" } else { $sign = "pos" } for my $x (@nums) { say $x } while ($running) { step() } ``` Perl adds two things Python does not have. **Postfix modifiers** let a simple statement carry its own condition or loop, which reads naturally for one-liners: ```perl use v5.42; say $_ for @nums; # loop, one statement say "negative" if $n < 0; # guarded statement $sum += $_ for @nums; # accumulate ``` And `unless`/`until` are the negated forms of `if`/`while`, for when the positive phrasing reads awkwardly: `say "ok" unless $error`. The gotcha: the ternary chains the same as Python's, but with `?`/`:`, not `if`/`else`: `my $sign = $n < 0 ? "neg" : $n == 0 ? "zero" : "pos"`. The C-style `?:` is the idiom; there is no `x if c else y` form. ## Subroutines `def` becomes `sub`. Modern Perl has signatures that look much like Python's parameter lists, including defaults. ```python def greet(name, greeting="Hello"): return f"{greeting}, {name}!" def total(*nums): return sum(nums) ``` ```perl use v5.42; sub greet ($name, $greeting = "Hello") { return "$greeting, $name!"; } say greet("Ada"); # Hello, Ada! say greet("Linus", "Hi"); # Hi, Linus! sub total (@nums) { my $sum = 0; $sum += $_ for @nums; return $sum; } say total(1, 2, 3, 4); # 10 ``` Keyword arguments map to a hash parameter, since `key => value` pairs flatten into a list that a `%opts` signature gathers up: ```perl use v5.42; sub connect_to (%opts) { return "host=$opts{host} port=$opts{port}"; } say connect_to(host => "localhost", port => 8080); # host=localhost port=8080 ``` The gotcha: a sub returns the value of its last expression even without `return`, and the return is **context-sensitive** (see [Context](#context)). If you `return @list`, the caller gets the list in list context but the *count* in scalar context. Be deliberate about what you return when it might be used either way. ## References Perl's arrays and hashes do not nest directly; you nest them through **references**, which are the Perl analogue of Python's object handles (in Python every container is already a reference, so this step is invisible to you there). A reference is a scalar that points at an aggregate. Take one with `\`, follow it with `->`. ```python data = {"name": "Ada", "langs": ["Perl", "Python"]} print(data["langs"][1]) # Python data["langs"].append("Ruby") ``` ```perl use v5.42; my $data = { name => "Ada", langs => ["Perl", "Python"] }; say $data->{name}; # Ada say $data->{langs}[1]; # Python push $data->{langs}->@*, "Ruby"; # postfix deref to push say "@{$data->{langs}}"; # Perl Python Ruby ``` `{ ... }` builds an anonymous hash reference and `[ ... ]` an anonymous array reference - the literal building blocks of nested data, matching Python's `{}` and `[]` literals. The `->` arrow follows a reference one step; between two subscripts it is optional, so `$data->{langs}[1]` and `$data->{langs}->[1]` are the same. Anonymous subs are closures, exactly like Python's `lambda` but with no single-expression limit: ```perl use v5.42; my $double = sub ($n) { $n * 2 }; say $double->(21); # 42 ``` The gotcha: the postfix deref forms (`->@*`, `->%*`, `->$*`) are the modern, readable way to dereference a whole aggregate, and `"@{ ... }"` is how you interpolate a dereferenced array into a string. The older `@$ref` and `${$ref}{key}` forms still work and appear in existing code, but reach for the arrow and postfix forms in new code. ## Regular expressions In Python regex is the `re` library. In Perl it is **syntax**: `=~` binds a pattern to a string, `m//` matches, `s///` substitutes. Capture groups land in `$1`, `$2`, and named captures in `%+`. ```python import re m = re.search(r"(\d{4})-(\d{2})-(\d{2})", s) if m: year, month, day = m.groups() t = re.sub(",", ";", "a,b,c") parts = "one,two,three".split(",") ``` ```perl use v5.42; my $s = "2026-06-20"; if ($s =~ /(\d{4})-(\d{2})-(\d{2})/) { say "$1 $2 $3"; # 2026 06 20 } if ($s =~ /(?\d{4})/) { say $+{year}; # 2026 } (my $t = "a,b,c") =~ s/,/;/g; # a;b;c my @parts = split /,/, "one,two,three"; # one two three ``` A `/g` match in list context returns *all* captures, which is the direct analogue of `re.findall`: ```perl use v5.42; my @nums = "x1y2z3" =~ /(\d)/g; # 1 2 3 say "@nums"; ``` The gotcha: `s///` modifies its target *in place* and returns the number of substitutions, not a new string. That is why the example copies first with `(my $t = "a,b,c")` and then substitutes on the copy - otherwise you would be editing a string literal. To get a new string without touching the original, use the `/r` flag: `my $t = $s =~ s/,/;/gr`. ## File and stream I/O The idiom is the three-argument `open` with a lexical filehandle, the direct counterpart of Python's `with open(...) as f:`. The angle-bracket operator `<$fh>` reads a line, the same role as iterating a Python file object. ```python with open("/tmp/sample.txt", "w") as f: for i in range(1, 4): f.write(f"line {i}\n") with open("/tmp/sample.txt") as f: for line in f: print("got:", line.rstrip()) ``` ```perl use v5.42; my $path = "/tmp/sample.txt"; open my $wh, '>', $path or die "cannot write: $!"; print {$wh} "line $_\n" for 1 .. 3; close $wh; open my $rh, '<', $path or die "cannot read: $!"; while (my $line = <$rh>) { chomp $line; say "got: $line"; # got: line 1 ... got: line 3 } close $rh; ``` To slurp every line at once, read the handle in list context: ```perl use v5.42; open my $fh, '<', "/tmp/sample.txt" or die $!; my @lines = <$fh>; # all lines, including newlines say scalar @lines; # 3 ``` The gotcha: a line read with `<$fh>` keeps its trailing newline; call `chomp` to strip it (Python's `line.rstrip()` reflex). And `open` returns false on failure rather than raising, so the `or die "...: $!"` idiom is mandatory - `$!` is the system error message, Perl's `errno`. ## Modules and packages `import` becomes `use`. The namespace separator is `::`, not `.`, and a module `Foo::Bar` lives at `Foo/Bar.pm` on the include path `@INC` (Perl's `sys.path`). A module's import list is a selection of names, much like `from module import name`. ```python from math import sqrt import json data = json.loads(text) ``` ```perl use v5.42; use List::Util qw(sum0 max first); say sum0(1 .. 10); # 55 say max(3, 7, 2); # 7 say first { $_ % 2 == 0 } (1, 3, 4, 5); # 4 ``` The standard library is large and much of what you would reach to PyPI for ships in core or on CPAN, the Perl package archive. `List::Util`, `Scalar::Util`, `POSIX`, `File::Spec`, and `Data::Dumper` are the everyday ones. The gotcha: `use` runs at *compile time*, not at the point in the file where it appears, so the order of `use` lines relative to your code does not matter the way `import` placement does. Importing is also opt-in per name: `use List::Util qw(sum0 max)` brings exactly `sum0` and `max` into your namespace, nothing else. ## Objects For new code, Perl 5.42 has a first-class `class` syntax that will look familiar coming from Python. Fields are declared with `field`, methods with `method`, and `$self` is available inside every method just as `self` is in Python (you do not list it as a parameter). ```python class Point: def __init__(self, x=0, y=0): self.x = x self.y = y def to_string(self): return f"({self.x}, {self.y})" def move(self, dx, dy): self.x += dx self.y += dy return self p = Point(x=3, y=4) print(p.to_string()) # (3, 4) p.move(1, 1) print(p.to_string()) # (4, 5) ``` ```perl use v5.42; use feature 'class'; class Point { field $x :param = 0; field $y :param = 0; method to_string { return "($x, $y)" } method move ($dx, $dy) { $x += $dx; $y += $dy; return $self; } } my $p = Point->new(x => 3, y => 4); say $p->to_string; # (3, 4) $p->move(1, 1); say $p->to_string; # (4, 5) ``` The constructor is generated for you: `:param` marks a field that the auto-built `new` accepts as a named argument, and `= 0` is its default. Inheritance is `:isa`: ```perl use v5.42; use feature 'class'; class Point3D :isa(Point) { field $z :param = 0; method z { return $z } } my $q = Point3D->new(x => 1, y => 2, z => 3); say $q->to_string, " z=", $q->z; # (1, 2) z=3 ``` The gotcha: the `class` feature is still marked experimental in Perl 5.42, and on `pperl` the "class is experimental" notices print to **stderr** even with `no warnings 'experimental::class'` in scope. They do not touch your program's output, so a script that prints to stdout behaves exactly as shown above; just expect the notices on the error stream. For the older `bless`-based object model you will meet in existing code, and for roles, inheritance details, and migration, see the [Object-oriented Programming guide](../oop/index). ## Error handling Python's `try`/`except` has two counterparts. The classic one wraps code in `eval { }` and inspects `$@` (the caught error) afterward: ```python try: risky(-1) except Exception as e: print("caught:", e) ``` ```perl use v5.42; my $result = eval { risky(-1) }; if ($@) { print "caught: $@"; # caught: negative! } ``` The modern form is `try`/`catch`, which reads almost exactly like Python's: ```perl use v5.42; use feature 'try'; try { risky(-4); } catch ($e) { print "caught: $e"; # caught: negative! } ``` where the raising side is `die`: ```perl use v5.42; sub risky ($n) { die "negative!\n" if $n < 0; return sqrt $n; } ``` The gotcha: `die` with a string that ends in `\n` produces exactly that message; `die` with a string that does *not* end in a newline gets `" at SCRIPT line N.\n"` appended automatically. End your error strings with `\n` when you want a clean message and omit it when you want the file-and-line trace. Note also the two error variables: `$@` is the last caught exception (Python's `except ... as e`), while `$!` is the last *system* error (a failed `open`, the `errno` string). ## Idioms A few patterns you will see daily and want in your fingers. Sort a list by a computed key (the Schwartzian transform - Python's `sorted(xs, key=f)` done by hand, decorate then sort then undecorate): ```perl use v5.42; my @words = qw(apple fig pear); my @by_length = map { $_->[1] } sort { $a->[0] <=> $b->[0] } map { [ length $_, $_ ] } @words; say "@by_length"; # fig pear apple ``` Build a hash from a list, the dict-comprehension analogue (`{w: len(w) for w in words}`): ```perl use v5.42; my @words = qw(apple pear fig); my %len = map { $_ => length $_ } @words; say "$_ => $len{$_}" for sort keys %len; # apple => 5 # fig => 3 # pear => 4 ``` And `pperl` is a command-line tool in the `awk`/`sed` niche. The implicit-loop one-liner is the Perl developer's everyday reflex: ```bash pperl -lane '$s += $F[-1]; END { print $s }' data.txt ``` The gotcha: read the [One-Liners guide](../oneliner/index) for the switches (`-n`, `-p`, `-a`, `-l`, `-e`) and the dozens of recipes built on them; it is one of the highest-leverage things to learn coming from a shell-and-Python background. ## Gotchas coming from Python The traps, collected for a final scan before you start writing: - **Sigils change with usage.** One element of `@a` is `$a[0]`; one value of `%h` is `$h{k}`. The container sigil and the element sigil differ. - **`.` joins strings, `+` adds numbers.** Never `+` two strings to concatenate; that coerces them to numbers and warns. - **Two comparison families.** `==`/`<`/`<=>` for numbers, `eq`/`lt`/ `cmp` for strings. Pick by operand type, not by variable. - **`sort` is string-sorted by default.** Pass `{ $a <=> $b }` for numeric order. - **Context.** `my $x = @a` is the *length*; `my ($x) = @a` is the *first element*. The parentheses are the whole difference. Re-read [Context](#context) when a value comes back the wrong shape. - **Truthiness surprises.** `"0"` is false, but `"00"` and `"0.0"` are **true**. Only `0`, `""`, `"0"`, and `undef` are false. - **`s///` edits in place** and returns a count; use the `/r` flag to get a new string instead. - **`open` does not raise.** Use `open ... or die "...: $!"`. - **`chomp` your line reads.** `<$fh>` keeps the trailing newline. - **Non-ASCII source needs `use utf8;`.** Under `use v5.42;` a literal non-ASCII byte in your source is a compile error without it. Keep string data ASCII unless you specifically add `use utf8;`. - **The `class` feature prints experimental notices to stderr** on `pperl`. Correct output, noisy error stream.