Perl gives you every tool you need to write clean, self-documenting, maintainable software. The problem is that the language is so expressive it rarely forces you to. This guide is about making the right choices — the habits, patterns, and idioms that turn Perl from a maintenance nightmare into a pleasure to work with.
"Always code as if the person who ends up maintaining your code is a violent psychopath who knows where you live." — Martin Golding (and every Perl old-timer who's read their own code six months later)
1. Start With the Holy Trinity
Before anything else, three pragma lines belong at the top of every Perl file you write. No exceptions. No excuses. Think of them as your seatbelt.
use strict; use warnings; use feature 'say'; # while you're at it # Or for modern Perl 5.36+: use v5.36; # enables strict, warnings, and more
strict catches the single most common source of Perl bugs: undeclared variables. Without it, a typo in a variable name silently creates a new variable with an empty value. warnings catches another enormous category of subtle errors — using undefined values, numeric/string conversion surprises, and deprecated behavior.
If you're on Perl 5.36 or later, use v5.36 enables strict and warnings automatically,
plus activates several modern features like the new builtin module and lexical
for loops. Use it. The Perl community has moved on from the JAPH one-liner era,
and so should your codebase.
2. Name Things Like a Human
Perl's sigils ($, @, %) already carry type information.
There's no reason to also compress your variable names into abbreviated noise.
Write for the reader, not the compiler.
my $u = get_user($id); my @r = grep { $_->{a} > 0 } @d; my %h; $h{$_->{n}}++ for @r;
my $user = get_user($user_id); my @active_items = grep { $_->{amount} > 0 } @data; my %count_by_name; $count_by_name{ $_->{name} }++ for @active_items;
Use underscores for multi-word names ($user_id, not $userId) — it's
conventional Perl style. Reserve CamelCase for package names. And
avoid Perl's many magical special variables ($_, $!, $/)
except in very short, self-contained scopes where their meaning is obvious.
The $_ trap
The default variable $_ is one of Perl's most powerful — and dangerous — features.
It's perfect inside a tight map or grep. But once a block grows beyond
three lines, localize it or name it. Implicit state is the enemy of readability.
# Fine: $_ is obvious in a one-liner my @names = map { uc } @raw_names; # Better: name it when the block grows my @processed = map { my $name = $_; $name = ucfirst( lc $name ); $name =~ s/-(\w)/'- ' . uc($1)/ge; $name } @raw_names;
3. Tame Your Regular Expressions
Regexes are where Perl's write-only reputation comes from most often — and fairly so. A single complex regex can do the work of twenty lines of imperative code, which is exactly why they become impossible to read six months later.
The /x modifier is your best friend. It lets you break a regex
across multiple lines with comments, turning a line of line noise into something
genuinely self-documenting.
if ( $email =~ /^[a-zA-Z0-9._%+\-]+\@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/ ) { ... }
my $EMAIL_RE = qr/ ^ [a-zA-Z0-9._%+\-]+ # local part \@ # literal at-sign [a-zA-Z0-9.\-]+ # domain name \. # dot before TLD [a-zA-Z]{2,} # TLD (2+ chars) $ /x; if ( $email =~ $EMAIL_RE ) { ... }
Notice the use of qr// to pre-compile and name the regex as a variable.
This is idiomatic for complex patterns — it separates the what (the pattern)
from the where (the place it's applied), and lets you reuse it across your codebase
without repeating yourself.
If a regex is longer than 30 characters, give it a name and use /x.
If it's longer than 60 characters, it should also have comments inside it.
If it spans multiple logical operations, break it into named sub-patterns with qr//.
4. Structure Code With Subroutines — Early and Often
One of the most common anti-patterns in Perl scripts is the five-hundred-line procedural script with no subroutines. It's tempting — Perl makes it trivially easy to just keep writing at the top level. Resist.
Subroutines do more than organize code. They force you to think about inputs and outputs, which naturally leads to cleaner logic. They make your code testable. And they give chunks of behavior a name, which is documentation in itself.
sub send_invoice { my ($customer, $items, $options) = @_; # Guard clauses at the top — fail fast, fail clearly die "customer is required\n" unless $customer; die "items must be a list\n" unless ref($items) eq 'ARRAY'; die "items list is empty\n" unless @$items; $options //= {}; # sane defaults with defined-or my $due_days = $options->{due_days} // 30; # ... business logic here, clean and uncluttered }
Adopt the guard clause pattern: validate inputs at the top of your subroutine
and return (or die) early. This eliminates deep nesting and keeps the happy path
visually dominant. The //= (defined-or assign) operator is your friend for defaults.
5. Embrace Modern OOP With Moo or Moose
Perl's built-in OOP — hand-rolling bless constructors, writing your own
accessors — is functional but verbose and error-prone. There's a better way, and
it's been the community standard for over a decade.
package User; use Moo; use Types::Standard qw( Str Int Bool ); has name => ( is => 'ro', isa => Str, required => 1 ); has email => ( is => 'ro', isa => Str, required => 1 ); has is_admin => ( is => 'rw', isa => Bool, default => 0 ); has age => ( is => 'ro', isa => Int ); sub greet { my ($self) = @_; return "Hello, I'm " . $self->name . '.'; } 1;
Moo is lightweight; Moose is feature-rich. Both give you
declarative attribute definitions, type checking, roles (Perl's answer to mixins), and
method modifiers. Code written with Moo reads almost like a specification — you can scan
the has declarations and immediately understand the object's data model.
For new Perl 5.40+ projects, the new class keyword (now stable) offers built-in OOP without any CPAN dependencies, with similar clarity.
6. Comment Intent, Not Mechanics
The worst Perl comments explain what the code does — something anyone who can read Perl already knows. Good comments explain why it does it that way.
# BAD: restates the code $count++; # increment count # GOOD: explains the decision # We increment before sleeping so the first tick fires immediately # rather than waiting for the full interval on startup (see issue #214) $count++; # GOOD: warns future readers # Do NOT sort here — the API requires items in insertion order. # Sorting breaks pagination tokens on the remote end. for my $item (@items) { ... }
7. Test With prove and Test::More
Untested Perl is unmaintainable Perl. Without tests, every refactor is a gamble. The good news: Perl's testing ecosystem is mature, elegant, and built right into the language's culture.
use strict; use warnings; use Test::More; use_ok('User'); my $user = User->new( name => 'Ada Lovelace', email => '[email protected]', ); isa_ok( $user, 'User' ); is( $user->name, 'Ada Lovelace', 'name is set correctly' ); is( $user->is_admin, 0, 'not admin by default' ); is( $user->greet, "Hello, I'm Ada Lovelace.", 'greet works' ); done_testing();
Run your tests with prove -lv t/. Add Test::Exception for
testing that errors are thrown correctly, and Test::Deep for complex
data structure comparisons. If you're on a larger project, Test2::Suite
is the modern successor to Test::More with improved output and tooling.