Learn Perl
Perl is a battle-tested, high-level, dynamic language created by Larry Wall in 1987 and famous for its expressive text-processing power, its motto "There's More Than One Way To Do It," and the vast CPAN module archive. This track teaches modern Perl 5 (version 5.40, released June 2024) the way it is written today: with use v5.40 turning on strict mode, warnings, subroutine signatures, and a clean built-in toolkit. You will set up the toolchain, master scalars, arrays, hashes and references, write subroutines with real signatures, wield Perl's legendary regular expressions, handle errors with native try/catch, build objects with the new core class syntax, and learn your way around CPAN. Like Guji and Python, Perl rewards you for running code early and often, so every lesson here is verified working code you can paste into a file and execute.
Setup and the Perl Toolchain
Install Perl, run scripts, and meet perldoc, cpanm, and perlbrew.
Perl 5 ships with virtually every Linux and macOS system, so you may already have it. Check your version from a terminal:
perl --version
You should see something like This is perl 5, version 40, subversion 1 (v5.40.1). The current stable major release is Perl 5.40, first released on 9 June 2024; the next, 5.42, arrived in July 2025. This track targets 5.40 because that single line use v5.40; unlocks all the modern conveniences at once. If your system Perl is older, install a fresh one in your home directory with perlbrew so you never touch the OS copy:
\curl -L https://install.perlbrew.pl | bash
perlbrew install perl-5.40.1
perlbrew switch perl-5.40.1
A Perl program is just a text file. Create hello.pl:
use v5.40;
say "Hello from Perl!";
Run it:
perl hello.pl
That first line is doing a lot. use v5.40; is a version bundle: it tells Perl "I want the 5.40 language," which automatically enables use strict; (forces you to declare variables and catches typos), use warnings; (flags suspicious code at runtime), subroutine signatures, the say function (a print that appends a newline), and the modern built-in toolkit. Always start scripts this way. To prove strict is working, try using an undeclared variable:
use v5.40;
$x = 5; # error: Global symbol "$x" requires explicit package name
Perl refuses to compile it. That single safety net catches an enormous class of bugs and is the single most important habit in modern Perl.
You will lean constantly on perldoc, the documentation tool bundled with every install. It reads the same manual the language ships with:
perldoc perlintro # the official beginner's introduction
perldoc perlfunc # every built-in function
perldoc -f split # docs for one function
perldoc List::Util # docs for an installed module
To install modules from CPAN (the Comprehensive Perl Archive Network, with over 200,000 modules), the friendliest client is cpanm:
cpan App::cpanminus # install cpanm itself, once
cpanm List::AllUtils # then install any module by name
Modern projects pin dependencies with a cpanfile and install them into a local local/ directory using Carton (Perl's answer to bundler or npm), so different projects never clash. For now, knowing perl, perldoc, and cpanm is enough to be productive.
One more habit: run a syntax check without executing, using the -c flag. It compiles your file and reports errors but runs nothing, which is perfect for a quick sanity pass:
perl -c hello.pl # prints "hello.pl syntax OK"
You now have a working Perl, a way to run and check scripts, and the documentation and module tools you will use every day. Read the official tour next: https://perldoc.perl.org/perlintro.
Scalars, Context, and Sigils
Numbers, strings, interpolation, and Perl's defining idea: context.
Perl's most distinctive idea is the sigil: the punctuation in front of a variable name tells you what kind of value it holds. A $ means a single value (a scalar), @ means an ordered list (an array), and % means a set of key/value pairs (a hash). This lesson covers scalars; the next covers the collections.
A scalar holds one thing: a number, a string, or a reference (a pointer to other data). Perl converts between numbers and strings automatically depending on how you use the value, so you rarely declare a "type." Declare scalars with my, which use strict requires:
use v5.40;
my $name = "Scout";
my $age = 31;
my $pi = 3.14159;
my $big = 1_000_000; # underscores are ignored, just for readability
say $name;
say $age + 9; # 40 - used in numeric context
String interpolation is everywhere in Perl. Inside double quotes, $ and @ variables expand into their values; single quotes are literal:
use v5.40;
my $name = "Scout";
say "Hello, $name!"; # Hello, Scout!
say 'Hello, $name!'; # Hello, $name! (literal)
Strings concatenate with . and repeat with x:
use v5.40;
my $line = "=" x 20; # twenty equals signs
say "ab" . "cd"; # abcd
Numeric and string operators are different operators, which keeps the meaning unambiguous since values are typeless. Use ==, !=, <, > to compare numbers, and eq, ne, lt, gt to compare strings:
use v5.40;
say "equal numerically" if 10 == 10.0;
say "equal as strings" if "10" eq "10";
say "not equal" if "10" ne "10.0"; # different text!
Now the deep idea: context. Every expression in Perl is evaluated in either scalar or list context, and many constructs behave differently in each. The classic example is an array used where a single value is expected, which yields its length:
use v5.40;
my @colors = ("red", "green", "blue");
my $count = @colors; # scalar context: $count is 3
say "There are $count colors";
say "Last index: $#colors"; # $#array is the highest index, here 2
You force scalar context explicitly with the scalar function, which is handy inside a string where context would otherwise be list:
use v5.40;
my @colors = ("red", "green", "blue");
say "I have " . scalar(@colors) . " colors";
Perl also has a special "default variable," $_, which many functions read from and write to when you do not name a variable. It makes short idioms read almost like English:
use v5.40;
for ("alpha", "beta", "gamma") {
say uc; # uc with no argument uppercases $_
}
Finally, the concept of truth. In a boolean test, the only false values are the empty string "", the string "0", the number 0, and undef (the "no value" value). Everything else, including "0.0" and "00", is true, which occasionally surprises newcomers, so test explicitly when in doubt:
use v5.40;
my $x; # undef
say "x is false" unless $x; # postfix 'unless' reads nicely
$x = "0";
say "still false" unless $x;
Context and sigils are the two ideas that make Perl code look the way it does. Once they click, the rest of the language falls into place. The authoritative reference for data types is the official perldata document: https://perldoc.perl.org/perldata.
Arrays, Hashes, and References
Build lists and dictionaries, then nest them with references.
Two collection types carry most of Perl's data: arrays (ordered lists, sigil @) and hashes (unordered key/value maps, sigil %). References let you nest them to build arbitrary structures.
An array is an ordered sequence indexed from zero. Note the sigil rule that trips up beginners: you use @ for the whole array but $ when you reach in for a single element, because one element is a scalar:
use v5.40;
my @fruits = ("apple", "banana", "cherry");
say $fruits[0]; # apple - single element, so $
say "count: " . scalar(@fruits);
push @fruits, "date"; # add to the end
my $last = pop @fruits; # remove from the end -> "date"
unshift @fruits, "apricot"; # add to the front
my $first = shift @fruits; # remove from the front
my @slice = @fruits[0, 2]; # a slice is several elements, so @
say "@slice"; # apple cherry
The workhorses for transforming arrays are grep (filter), map (transform), and sort. Each runs a block where $_ is the current element:
use v5.40;
my @nums = (5, 2, 9, 1, 7);
my @big = grep { $_ > 4 } @nums; # 5, 9, 7
my @doubled = map { $_ * 2 } @nums; # 10, 4, 18, 2, 14
my @sorted = sort { $a <=> $b } @nums; # 1, 2, 5, 7, 9 (numeric)
say "big: @big";
say "doubled: @doubled";
say "sorted: @sorted";
sort defaults to string order; the { $a <=> $b } block sorts numerically (use cmp for explicit string order). $a and $b are the two elements being compared.
A hash maps keys to values. Use % for the whole hash and $ plus curly braces for one value. The => "fat comma" is just a comma that auto-quotes the word on its left, so it reads like a label:
use v5.40;
my %ages = (
Scout => 31,
Lumi => 7,
);
$ages{Holt} = 42; # add or update one entry
say "Scout is $ages{Scout}";
say "knows Lumi" if exists $ages{Lumi}; # test for a key
delete $ages{Holt}; # remove a key
for my $name (sort keys %ages) { # iterate in sorted key order
say "$name => $ages{$name}";
}
keys and values return lists; hashes have no inherent order, so sort the keys when you want stable output.
Arrays and hashes only hold scalars, so to nest them you store references. A reference is a scalar that points at a larger structure. Create an array reference with [...] and a hash reference with {...}, and follow the arrow -> to dig in:
use v5.40;
my $person = {
name => "Scout",
roles => ["admin", "developer"], # value is an array reference
pets => { dog => "Lumi" }, # value is a hash reference
};
say $person->{name}; # Scout
say $person->{roles}[0]; # admin (arrow optional between brackets)
say $person->{pets}{dog}; # Lumi
To loop over a referenced array, dereference it. The modern postfix dereference syntax reads left to right and is the idiom you will see in current code:
use v5.40;
my $data = {
users => [
{ name => "Scout", roles => ["admin", "dev"] },
{ name => "Lumi", roles => ["bot"] },
],
};
for my $u ($data->{users}->@*) { # @* dereferences the arrayref
say "$u->{name}: " . join(", ", $u->{roles}->@*);
}
This prints Scout: admin, dev then Lumi: bot. With references you can model JSON-like trees, graphs, and records of any depth.
For a deep, example-driven walk through nested structures, read the official data-structures cookbook, perldsc: https://perldoc.perl.org/perldsc.
Subroutines and Signatures
Define functions with named parameters, defaults, and slurpy lists.
Functions in Perl are called subroutines, declared with sub. For most of Perl's history a subroutine received its arguments as a flat list in the special array @_ and had to unpack them by hand. Since Perl 5.36 (and on automatically under use v5.36 or later), subroutine signatures are a stable feature, giving you named parameters that read like every other modern language.
Here is a subroutine with two parameters, the second having a default value:
use v5.40;
sub greet ($name, $greeting = "Hello") {
return "$greeting, $name!";
}
say greet("Scout"); # Hello, Scout!
say greet("Scout", "Howdy"); # Howdy, Scout!
The signature ($name, $greeting = "Hello") declares the parameters; $greeting is optional because it has a default. Perl checks the argument count for you: passing too many arguments is a fatal error, which catches a common class of mistakes that the old @_ style let slip through silently.
To accept a variable number of arguments, end the signature with a slurpy array or hash that swallows everything remaining:
use v5.40;
sub total ($first, @rest) {
my $sum = $first;
$sum += $_ for @rest; # postfix for-loop over the slurpy array
return $sum;
}
say total(10); # 10
say total(1, 2, 3, 4); # 10
A slurpy hash is perfect for named options:
use v5.40;
sub make_user ($name, %opts) {
my $role = $opts{role} // "member"; # // is "defined-or"
my $active = $opts{active} // 1;
return "$name ($role)" . ($active ? "" : " [inactive]");
}
say make_user("Scout", role => "admin");
say make_user("Lumi", active => 0);
The // operator above is the defined-or operator: it returns its left side unless that side is undef, in which case it returns the right side. It is the clean way to supply defaults for options the caller omitted, and unlike || it treats 0 and "" as real values rather than as "missing."
Subroutines return the value of their last expression even without an explicit return, but writing return makes intent obvious. Return context matters: a subroutine can hand back a list, which the caller can capture into an array or into several scalars at once:
use v5.40;
sub min_max (@nums) {
my @sorted = sort { $a <=> $b } @nums;
return ($sorted[0], $sorted[-1]); # -1 is the last element
}
my ($lo, $hi) = min_max(5, 2, 9, 1);
say "low=$lo high=$hi"; # low=1 high=9
Subroutines are also values you can store and pass around. A reference to an anonymous subroutine uses sub { ... }, and you call it through its reference with ->(). This is how you write higher-order code, callbacks, and dispatch tables:
use v5.40;
my %dispatch = (
add => sub ($a, $b) { $a + $b },
mul => sub ($a, $b) { $a * $b },
);
for my $op (qw(add mul)) { # qw() is a quoted word-list shortcut
say "$op: " . $dispatch{$op}->(6, 7);
}
That prints add: 13 then mul: 42. The qw(add mul) is shorthand for ("add", "mul"), a constant Perlism worth memorizing.
Signatures, defaults, slurpy parameters, and the defined-or operator together make modern Perl subroutines concise and safe. The full reference, including older @_ unpacking you will still meet in legacy code, is perlsub: https://perldoc.perl.org/perlsub.
Regular Expressions: Perl's Superpower
Match, capture, and substitute text with the regex engine Perl made famous.
Perl's reputation as the text-processing language comes from its regular expressions, which are so influential that "Perl Compatible Regular Expressions" (PCRE) became a standard adopted far beyond Perl itself. Regexes are built into the language with first-class operators, so you rarely need any module to use them.
The match operator is =~ paired with a /pattern/. It returns true when the pattern is found:
use v5.40;
my $text = "The quick brown fox";
if ($text =~ /quick/) {
say "found 'quick'";
}
if ($text =~ /\bfox\b/) { # \b is a word boundary
say "found the word fox";
}
Wrap part of a pattern in parentheses to capture it. After a successful match the captured groups are available in $1, $2, and so on:
use v5.40;
my $date = "2026-06-16";
if ($date =~ /^(\d{4})-(\d{2})-(\d{2})$/) {
say "year=$1 month=$2 day=$3"; # year=2026 month=06 day=16
}
Here \d matches a digit, {4} means "exactly four," and the ^ and $ anchor the match to the start and end of the string. To pull every match out of a string, add the /g (global) flag and assign to a list; each capture group becomes a list element:
use v5.40;
my $log = "scout\@range.dev contacted lumi\@range.dev";
my @emails = $log =~ /([\w.]+\@[\w.]+)/g;
say "found: @emails"; # found: scout@range.dev lumi@range.dev
\w matches a "word" character (letters, digits, underscore), and \@ escapes the literal @ so Perl does not treat it as an array sigil inside the double-quoted-style pattern.
The substitution operator s/pattern/replacement/ rewrites matched text. With /g it replaces every occurrence; the match happens in place on the variable to its left:
use v5.40;
my $text = "Contact scout\@range.dev or lumi\@range.dev";
(my $masked = $text) =~ s/\@[\w.]+/\@example.com/g;
say $masked; # Contact scout@example.com or lumi@example.com
The (my $masked = $text) =~ s/.../.../ idiom copies the original into a new variable and edits the copy, leaving $text untouched. Inside the replacement you can reference captures with $1, and the /r flag offers an even cleaner approach: it returns the modified string instead of changing the original, so it composes nicely:
use v5.40;
my $name = "scout_of_the_range";
my $title = $name =~ s/_/ /gr; # /r returns a new string
say ucfirst $title; # Scout of the range
A few flags you will reach for constantly:
/imakes the match case-insensitive:/scout/imatches "Scout" and "SCOUT"./xlets you add whitespace and comments inside a pattern for readability./mmakes^and$match at line boundaries within a multi-line string./smakes.match newlines too.
Combined with the tr/// transliteration operator (good for counting or swapping individual characters) and named captures (?<year>\d{4}) accessed through the %+ hash, the regex engine handles parsing jobs that would be many lines elsewhere. A whole tutorial ships with Perl; read it with perldoc perlretut, and keep the quick-reference perldoc perlre open while you work: https://perldoc.perl.org/perlretut.
Error Handling with try/catch and die
Throw exceptions with die and catch them with native try/catch.
Perl signals errors by throwing an exception, which it calls "dying." The built-in die function raises an exception, and historically you caught it with an eval block, inspecting the special $@ variable afterward. Modern Perl gives you a clean try/catch syntax that became stable in Perl 5.40, so you can write exception handling that looks like the rest of the language.
To raise an error, call die with a message. End the message with "\n" so Perl does not append the file and line number:
use v5.40;
sub safe_sqrt ($n) {
die "cannot take sqrt of negative number: $n\n" if $n < 0;
return sqrt($n);
}
Wrap risky code in a try block and handle failures in the matching catch. The caught exception is bound to the variable you name in the catch parentheses:
use v5.40;
sub safe_sqrt ($n) {
die "cannot take sqrt of negative number: $n\n" if $n < 0;
return sqrt($n);
}
try {
say safe_sqrt(16); # 4
say safe_sqrt(-1); # throws
say "unreachable";
}
catch ($e) {
chomp $e; # strip the trailing newline
say "caught error: $e";
}
say "program keeps running";
This prints 4, then caught error: cannot take sqrt of negative number: -1, then program keeps running. Execution jumps out of the try the instant die fires, skipping the "unreachable" line, and resumes after the catch. You can add an optional finally block that always runs, whether or not an exception was thrown, which is ideal for cleanup. The bare try/catch pair is stable in 5.40, but adding finally is still flagged experimental, so silence that one warning:
use v5.40;
no warnings 'experimental::try'; # only needed because of finally
try {
say "doing work";
die "boom\n";
}
catch ($e) {
chomp $e;
say "handling: $e";
}
finally {
say "always cleans up";
}
Exceptions do not have to be strings. You can die with a reference or an object, which lets you carry structured information such as an error code and attach behavior. Catching it gives you the whole object back:
use v5.40;
try {
die { code => 404, message => "not found" }; # die with a hashref
}
catch ($e) {
if (ref $e eq "HASH") {
say "error $e->{code}: $e->{message}"; # error 404: not found
}
else {
say "string error: $e";
}
}
The ref function tells you what kind of reference you caught ("HASH", "ARRAY", or a class name for objects), so you can dispatch on error type. For production code, the CPAN Throwable and Exception::Class modules give you ready-made exception classes with stack traces, and the venerable Carp module's croak and confess report errors from the caller's point of view, which is what library authors use instead of plain die.
Two everyday habits round this out. First, many built-ins like open return false on failure rather than dying, so the classic idiom is open(my $fh, "<", $file) or die "cannot open $file: $!"; - the $! variable holds the operating-system error string. Second, prefer try/catch over the old eval { ... }; if ($@) { ... } pattern in new code, because try/catch avoids subtle bugs around $@ being clobbered.
The official documentation for the feature lives in perlsyn under "Try Catch Exception Handling," and the broader topic in perldoc perlvar (for $@ and $!): https://perldoc.perl.org/perlsyn#Try-Catch-Exception-Handling.
Object-Oriented Perl with the class Feature
Build objects with the modern core class/field/method syntax (Corinna).
Perl gained a brand-new, modern object system in the core language with the class feature (developed under the codename "Corinna"), introduced in Perl 5.38 and refined in 5.40 with additions like the __CLASS__ keyword and the :reader attribute. It is still marked experimental as of 5.40, so you enable it with use experimental 'class'; and accept that the syntax may evolve, but it is already the clearest way to write objects in Perl and it is where the language is heading.
A class is declared with the class keyword. State lives in field variables (private by default), and behavior lives in method blocks. Constructors and the implicit $self are provided for you:
use v5.40;
use experimental 'class';
class Point {
field $x :param = 0; # :param lets new() set it; = 0 is the default
field $y :param = 0;
method to_string {
return "($x, $y)";
}
method move ($dx, $dy) { # methods take signatures, like subs
$x += $dx;
$y += $dy;
return $self; # $self is implicit; return it to chain calls
}
}
my $p = Point->new(x => 3, y => 4);
say $p->to_string; # (3, 4)
$p->move(1, -1);
say $p->to_string; # (4, 3)
Three things to notice. The :param attribute on a field makes it a named constructor argument, so Point->new(x => 3, y => 4) just works without you writing a new method. Inside a method, the field variables $x and $y are directly in scope - no $self->{x} bookkeeping. And $self is always available, so returning it from move lets you chain calls fluently.
To expose a field for reading without writing a getter by hand, add the :reader attribute (new and very convenient in 5.40). It generates a method of the same name:
use v5.40;
use experimental 'class';
class Temperature {
field $celsius :param :reader = 0;
method fahrenheit {
return $celsius * 9 / 5 + 32;
}
}
my $t = Temperature->new(celsius => 100);
say $t->celsius; # 100 - generated by :reader
say $t->fahrenheit; # 212
Inheritance uses the :isa attribute on the class declaration. A subclass inherits fields and methods, and an ADJUST block lets you run setup logic right after the object is constructed:
use v5.40;
use experimental 'class';
class Animal {
field $name :param :reader;
method speak { return "$name makes a sound"; }
}
class Dog :isa(Animal) {
method speak { return $self->name . " barks"; } # override
}
my $d = Dog->new(name => "Lumi");
say $d->speak; # Lumi barks
say ref $d; # Dog
say $d->isa("Animal") ? "is an Animal" : "no"; # is an Animal
The subclass Dog reuses Animal's $name field and its :reader, while overriding speak. The isa method confirms the inheritance relationship at runtime.
You will still encounter Perl's original object system in the wild: there, a class is just a package, an object is a reference "blessed" into that package with bless, and methods are ordinary subroutines whose first argument is the invocant. That style, often combined with the powerful CPAN frameworks Moose or the lighter Moo, powers a huge amount of existing code and remains fully supported. But for new code on Perl 5.38+, the core class feature is simpler, faster, and encapsulates state properly, so prefer it.
The official tutorial for the new syntax is perlclass, with the conceptual guide in perlclasstut: https://perldoc.perl.org/perlclass.
CPAN, Built-ins, and the Ecosystem
Use the new builtin toolkit, pull in CPAN modules, and structure a project.
Perl's greatest practical asset is CPAN, the Comprehensive Perl Archive Network: a single, curated repository of more than 200,000 reusable modules covering web frameworks, database drivers, JSON and XML, dates, cryptography, and almost anything else. "Don't reinvent it, find it on CPAN" is the working motto. Before reaching for CPAN, though, know that Perl 5.40 ships a clean builtin module of small utilities that used to require external code. Under use v5.40 these built-ins are already enabled (you only silence the experimental warning):
use v5.40;
no warnings 'experimental::builtin';
say true ? "yes" : "no"; # boolean values
say trim(" hello "); # -> "hello"
say builtin::ceil(3.2); # 4
say builtin::floor(3.8); # 3
my @list = (3, 1, 2);
say "indexed:";
say " $_" for indexed @list; # pairs of (index, value)
For list manipulation, the core List::Util module is indispensable and ships with Perl, so no installation is needed. Import exactly the functions you want:
use v5.40;
use List::Util qw(sum max min first uniq);
my @nums = (5, 2, 9, 2, 7, 9);
say "sum: " . sum(@nums); # 34
say "max: " . max(@nums); # 9
say "first big: " . first { $_ > 4 } @nums; # 5
say "unique: " . join(",", uniq @nums); # 5,2,9,7
When the standard library is not enough, install from CPAN with cpanm (covered in lesson one):
cpanm JSON::PP DateTime Path::Tiny
Then use them. Here JSON::PP (a JSON encoder/decoder that ships with Perl) round-trips a data structure, the kind of task you do constantly:
use v5.40;
use JSON::PP;
my $data = {
name => "Scout",
roles => ["admin", "developer"],
active => JSON::PP::true,
};
my $json = encode_json($data); # Perl structure -> JSON text
say $json;
my $back = decode_json($json); # JSON text -> Perl structure
say "first role: " . $back->{roles}[0]; # admin
Real projects organize code into modules - files ending in .pm whose package name mirrors their path. A file lib/Range/Greeter.pm defines package Range::Greeter; and is loaded with use Range::Greeter; when lib is on the module search path (perl -Ilib script.pl). Each module ends with a true value (the traditional 1;) so Perl knows it loaded successfully:
# lib/Range/Greeter.pm
package Range::Greeter;
use v5.40;
use Exporter 'import';
our @EXPORT_OK = ('greet');
sub greet ($name) { return "Hello, $name!"; }
1;
To manage a project's dependencies reproducibly, list them in a cpanfile and install them into a project-local directory with Carton or cpanm --installdeps, so each project carries its own pinned versions:
# cpanfile
requires 'JSON::PP';
requires 'DateTime', '>= 1.59';
requires 'Path::Tiny';
cpanm --installdeps . # read cpanfile, install everything
Two tools keep your own code healthy. Perl::Critic (perlcritic script.pl) flags style and safety problems against the community's Perl Best Practices guidelines, and Perl::Tidy (perltidy script.pl) reformats code to a consistent style, much like gofmt or black. Both install from CPAN and slot neatly into an editor or CI pipeline.
That completes the tour: you can set up Perl, work with scalars and collections, write subroutines with signatures, command the regex engine, handle exceptions, build objects with the modern class feature, and pull the whole ecosystem in through CPAN. Browse the archive yourself and search for any task at https://metacpan.org, the modern front end to CPAN, and keep perldoc perlmodlib handy for what ships with core Perl.