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:

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#

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.

name = "Ada"
langs = ["Python", "Perl"]
ages = {"Ada": 36, "Linus": 56}
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).

name = "Ada"
greeting = f"Hello, {name}"
shout = "AB" * 3
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.

print(7 / 2)        # 3.5
print(7 // 2)       # 3
print(2 ** 10)      # 1024
print(10 % 3)       # 1
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.

nums = [1, 2, 3, 4, 5]
nums.append(6)
last = nums.pop()
print(len(nums))    # 5
print(nums[1:3])    # [2, 3]
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.

squares = [x * x for x in nums]
evens   = [x for x in nums if x % 2 == 0]
desc    = sorted(nums, reverse=True)
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}.

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])
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.

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:

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:

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.

use v5.42;
for my $v (0, 1, "", "0", "00", "0.0", "false") {
    say "'$v' is ", ($v ? "true" : "false");
}

This prints, exactly:

'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 ""):

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.

if n < 0:
    sign = "neg"
elif n == 0:
    sign = "zero"
else:
    sign = "pos"

for x in nums:
    print(x)

while running:
    step()
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:

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.

def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

def total(*nums):
    return sum(nums)
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:

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). 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 ->.

data = {"name": "Ada", "langs": ["Perl", "Python"]}
print(data["langs"][1])     # Python
data["langs"].append("Ruby")
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:

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 %+.

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(",")
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 =~ /(?<year>\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:

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.

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())
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:

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.

from math import sqrt
import json
data = json.loads(text)
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).

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)
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:

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.

Error handling#

Python’s try/except has two counterparts. The classic one wraps code in eval { } and inspects $@ (the caught error) afterward:

try:
    risky(-1)
except Exception as e:
    print("caught:", e)
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:

use v5.42;
use feature 'try';

try {
    risky(-4);
}
catch ($e) {
    print "caught: $e";     # caught: negative!
}

where the raising side is die:

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):

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}):

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:

pperl -lane '$s += $F[-1]; END { print $s }' data.txt

The gotcha: read the One-Liners guide 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 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.