The 60 Perl questions interviewers actually ask, with direct answers, runnable code, and what the interviewer is listening for. Grouped for freshers, intermediate, and experienced rounds.
60 questions with answersKey Takeaways
Perl is a high-level, interpreted programming language created by Larry Wall and first released in 1987, built to make text processing and report generation easy. It's dynamically typed, mixes procedural, object-oriented, and functional styles, and its regular expression engine is so strong that it shaped regex support in nearly every language since. Perl keeps a role in system administration, bioinformatics, log parsing, and legacy web backends. In interviews, Perl questions probe regular expressions, references and nested data structures, context (the same expression behaves differently in list versus scalar context), and idioms like the default variable $_. This page collects the 60 questions that come up most, each with a direct answer and runnable code. If you're building fundamentals, the official perlintro document on perl.org is the canonical starting point; increasingly the first round is a live AI coding interview, so pair this bank with our AI interview preparation guides for the format side.
Watch: Perl Complete Course | Full 5 Hours Tutorial for Beginners to Advanced [2025 Edition]
Video: Perl Complete Course | Full 5 Hours Tutorial for Beginners to Advanced [2025 Edition] (Study Automation Academy, YouTube)
Test yourself and earn a certificate
10 quick questions. Score 70%+ to download your Perl certificate.
The fundamentals every entry-level round checks. If any answer here surprises you, that's your study list.
Perl is a high-level, interpreted language created by Larry Wall in 1987, built for text processing and report generation. It's dynamically typed and blends procedural, object-oriented, and functional styles.
It's still used for system administration, log and file parsing, bioinformatics, and legacy web backends. Its regular expression support is strong enough that Perl-compatible regex (PCRE) became a standard other languages copied.
Key point: A one-line definition plus two concrete use cases beats a memorized feature list. Interviewers open with this to hear how you organize an answer.
Watch a deeper explanation
Video: Perl Tutorial (Derek Banas, YouTube)
Scalars, arrays, and hashes. A scalar ($) holds a single value: a number, string, or reference. An array (@) is an ordered list indexed by position. A hash (%) is an unordered set of key-value pairs.
The sigil in front of the variable tells you the type, and the same base name can carry different types: $count, @count, and %count are three separate variables.
my $name = "Asha"; # scalar
my @teams = ("eng", "ops"); # array
my %ages = (Asha => 31, Ben => 26); # hash
print $ages{Asha}; # 31| Type | Sigil | Holds |
|---|---|---|
| Scalar | $ | One value: number, string, or reference |
| Array | @ | Ordered list, indexed from 0 |
| Hash | % | Unordered key-value pairs |
Key point: The follow-up is usually 'what sigil do you use to read one array element?'. the technical answer is $ (my @a; then $a[0]), which surprises beginners.
A sigil is the symbol before a variable name: $ for scalar, @ for array, % for hash. It tells the parser what kind of value you mean.
When you access a single element, you're asking for one scalar value, so you use $ even on an array or hash: $items[0] reads one element of @items, and $prices{apple} reads one value from %prices. The sigil follows the value you get back, not the container.
my @items = ("a", "b", "c");
my %prices = (apple => 3, pear => 2);
print $items[0]; # a (one scalar from the array)
print $prices{apple}; # 3 (one scalar from the hash)
print "@items"; # a b c (the whole array)Key point: This trips up nearly every beginner. Explaining 'the sigil matches the value, not the container' shows real understanding.
use strict forces you to declare variables with my (or our), bans symbolic references, and blocks bareword confusion, so typos become compile errors instead of silent bugs. use warnings reports suspicious runtime behavior like using an undefined value or a number where text was expected.
Together they catch a large share of beginner mistakes before they reach production. Putting both at the top of every script is the mark of someone who's worked on real Perl.
use strict;
use warnings;
my $count = 0;
$conut++; # compile error under strict: not declared
print $missing; # warning under warnings: undefined valueKey point: If a candidate writes Perl without these two lines, interviewers notice. Volunteering them in practice is a quiet credibility signal.
Context is how Perl decides what an expression means based on where it's used. The same expression can produce different results in list context versus scalar context.
The clearest example is an array: in list context it gives all its elements, but in scalar context it gives its count. Assigning an array to a scalar (my $n = @array) forces scalar context and returns the length, which surprises people expecting the first element.
my @nums = (10, 20, 30);
my @copy = @nums; # list context: (10, 20, 30)
my $n = @nums; # scalar context: 3 (the count)
my ($first) = @nums; # list context on the right: 10Key point: Context is the single most Perl-specific concept. Interviewers use my $n = @array to see if you actually understand it.
== compares numbers; eq compares strings. Perl keeps them separate because a scalar can be read as either, and the operator decides which comparison happens.
Using the wrong one is a common bug: "10" == "10.0" is true (numeric), but "10" eq "10.0" is false (string). The full pairs are ==, !=, <, > for numbers and eq, ne, lt, gt for strings.
print "10" == "10.0" ? "yes" : "no"; # yes (numeric)
print "10" eq "10.0" ? "yes" : "no"; # no (string)
print "abc" eq "abc" ? "yes" : "no"; # yes| Comparison | Numeric | String |
|---|---|---|
| Equal | == | eq |
| Not equal | != | ne |
| Less than | < | lt |
| Greater than | > | gt |
Key point: Expect a trick: 'is "10" equal to "10.0"?'. The right answer is 'it depends which operator you use', which is exactly the point.
$_ is the default variable. Many functions and constructs use it implicitly when you don't give an argument: print with no argument prints $_, a bare regex matches against $_, and a foreach loop without a named variable sets $_ each pass.
It makes short scripts compact but can hurt readability in longer code, so name your loop variables when clarity matters.
for (1, 2, 3) {
print; # prints $_
print "\n";
}
my @words = qw(cat dog);
for (@words) {
print uc, "\n"; # uc uses $_ : CAT, DOG
}chomp removes a trailing newline (more precisely, the current input record separator $/) if it's there, and returns the number of characters removed. chop removes the very last character no matter what it is and returns that character.
In practice you almost always want chomp, to strip the newline off a line read from input without touching real data.
my $line = "hello\n";
chomp $line; # "hello" (newline gone)
my $word = "hello";
chop $word; # "hell" (last char gone)Key point: The follow-up is 'what does chomp remove if there's no newline?'. Answer: nothing, and it returns 0.
my declares a lexical variable, visible only inside the enclosing block or file, and is what you should reach for by default. our declares a package (global) variable and makes it accessible by its short name under use strict. local temporarily saves and replaces the value of a global variable for the duration of the current block, restoring it afterward.
The common confusion is thinking local creates a local variable; it doesn't. It dynamically scopes an existing global, which is a different and older mechanism.
our $config = "prod";
sub run {
local $config = "test"; # temporary override
inner(); # sees "test"
}
run();
print $config; # "prod" againKey point: Knowing that local is dynamic scoping, not lexical scoping, is the senior-sounding distinction here.
push and pop work at the end of the array; shift and unshift work at the front. push adds to the end, pop removes from the end, unshift adds to the front, and shift removes from the front.
splice is the general tool for inserting or removing anywhere in the middle by index. Knowing which four verbs map to which ends is a routine warm-up question.
my @q = (2, 3);
push @q, 4; # (2, 3, 4)
unshift @q, 1; # (1, 2, 3, 4)
my $last = pop @q; # 4, @q = (1, 2, 3)
my $first = shift @q; # 1, @q = (2, 3)| Operation | Front | Back |
|---|---|---|
| Add | unshift | push |
| Remove | shift | pop |
A hash stores key-value pairs with no guaranteed order. You set and read values with $hash{key}, get all keys with keys %hash, all values with values %hash, and test for a key with exists.
delete removes a pair, and each lets you iterate key-value pairs. Because keys are unique, assigning to an existing key overwrites it.
my %age = (Asha => 31, Ben => 26);
$age{Cara} = 29; # add
print $age{Asha}; # 31
print "yes" if exists $age{Ben}; # yes
delete $age{Ben}; # remove
for my $name (keys %age) {
print "$name: $age{$name}\n";
}print writes its arguments as-is. printf writes formatted output using a format string, like C's printf, which is how you control decimals and padding. say (available with use feature 'say' or use v5.10) is print with an automatic trailing newline.
For simple output say saves typing the newline; for numeric formatting printf is the tool.
use v5.10;
print "no newline";
say "with newline"; # adds \n
printf "%.2f\n", 3.14159; # 3.14Open a filehandle with a three-argument open, then read line by line with the diamond operator in a while loop. Reading STDIN uses the special filehandle <STDIN> or the bare <> in a loop.
Always check whether open succeeded, chomp each line to drop the newline, and close the handle when done. The three-argument form with a lexical handle is the modern style.
open(my $fh, "<", "data.txt") or die "cannot open: $!";
while (my $line = <$fh>) {
chomp $line;
print "got: $line\n";
}
close $fh;Key point: Interviewers watch for the or die "...: $!" check. Skipping error handling on open is the tell of someone who hasn't shipped Perl.
length for size, substr to extract or replace a slice, index and rindex to find positions, uc and lc for case, reverse, split to break a string into a list, and join to combine a list into a string.
sprintf builds a formatted string without printing it. These handle most day-to-day text work before you even reach regular expressions.
my $s = "interview";
print length $s; # 9
print substr($s, 0, 5); # inter
print uc $s; # INTERVIEW
print index($s, "view"); # 5
my @parts = split /,/, "a,b,c"; # (a, b, c)split takes a pattern and a string and returns a list of the pieces between matches, so split /,/, "a,b,c" gives ("a", "b", "c"). join does the reverse: it takes a separator string and a list and glues them into one string.
split's first argument is a regex, which means you can split on whitespace runs, punctuation, or any pattern. A special case, split ' ', collapses leading whitespace and splits on any whitespace run.
my @cols = split /\t/, "id\tname\trole";
my $csv = join ",", @cols; # "id,name,role"
my @words = split " ", " spaced out "; # ("spaced", "out")Conditionals: if, elsif, else, and unless (the negated if). Loops: while, until (negated while), for (C-style), and foreach for iterating a list. Perl also allows statement modifiers, a trailing condition on a single statement.
unless and until read naturally for negative conditions, and the statement-modifier form (print $_ for @list) is idiomatic for one-liners.
print "ok\n" if $count > 0;
print "empty\n" unless @list;
for my $n (1..3) { print $n; } # 123
my $i = 0;
until ($i >= 3) { print $i; $i++; } # 012Perl treats these as false: the number 0, the string "0", the empty string "", and undef. Everything else is true, including the string "0.0", the string "00", and any non-empty string.
This catches people: "0" is false but "0.0" is true because it's a non-empty, non-"0" string. When in doubt, that's the rule to state.
print "F" unless 0; # false
print "F" unless "0"; # false
print "F" unless ""; # false
print "T" if "0.0"; # true (non-empty, not "0")
print "T" if "00"; # trueKey point: The trap question is 'is the string "0.0" true or false?'. Answer: true, and knowing why (it's not "0" or empty) is the point.
undef is Perl's representation of no value: an unassigned variable, a missing hash key, or a value you explicitly cleared with undef $x. It indicates 0 in numeric context and "" in string context, usually with a warning.
Test for it with defined, not with a truth test, because a valid 0 or empty string is defined but false. defined $x is the correct check for presence.
my $x;
print "undef\n" unless defined $x; # undef
my %h = (score => 0);
print "has key\n" if defined $h{score}; # has key (0 is defined)$_ is the default variable. @_ holds a subroutine's arguments. $0 is the script name. $! is the last system error message. $@ holds the error from the most recent eval. @ARGV holds command-line arguments, and %ENV holds environment variables.
You don't need all of them memorized, but $_, @_, $!, and $@ come up constantly, so those four are worth knowing cold.
| Variable | Holds |
|---|---|
| $_ | Default variable for many operations |
| @_ | Arguments passed to the current subroutine |
| $! | Last operating-system error |
| $@ | Error from the last eval block |
| @ARGV | Command-line arguments |
A one-liner is a whole program passed on the command line with -e, often with -n or -p to loop over input lines and -i to edit files in place. They turn Perl into a fast text tool for filtering, substituting, and reporting.
-n wraps your code in a read loop, -p does the same but prints each line, and combined with a substitution they replace many sed and awk tasks.
# uppercase every line
perl -pe '$_ = uc' file.txt
# print lines matching a pattern
perl -ne 'print if /error/' log.txt
# in-place replace
perl -i -pe 's/foo/bar/g' config.txtKey point: Being able to write a -pe substitution one-liner on the spot is a small but memorable signal of hands-on Perl.
For candidates with working experience: regex depth, references, subroutines, and the idioms that separate users from understanders.
m// matches a pattern against a string, s/// substitutes matched text with a replacement, and tr/// transliterates characters one for one. You bind a pattern to a string with =~ (or !~ to negate).
Common modifiers: /g for all matches, /i for case-insensitive, /m for multiline anchors, /s to let . match newlines, and /x to allow whitespace and comments in the pattern. Regex is built into the language, which is a large part of why Perl exists.
my $s = "Order 42 shipped";
print "match\n" if $s =~ /\d+/; # match
$s =~ s/shipped/delivered/; # substitution
my $count = ($s =~ tr/o/o/); # count the letter oKey point: Regex comes up in almost every Perl round. Knowing the four common modifiers cold (g, i, m, x) is expected, not optional.
Watch a deeper explanation
Video: Perl Basics #15: Regular Expressions (PerlTechStack, YouTube)
Parentheses in a pattern capture the matched text into $1, $2, and so on, numbered by opening paren. In list context a match returns the captures directly. Named captures with (?<name>...) store into %+ for readability.
A backreference like \1 inside the pattern matches the same text a group already captured, which is how you find repeated words or balanced quotes.
my $date = "2026-07-04";
if ($date =~ /(\d{4})-(\d{2})-(\d{2})/) {
print "year $1, month $2, day $3\n";
}
# named capture
$date =~ /(?<y>\d{4})/;
print $+{y}; # 2026/g replaces every match, not just the first. /e treats the replacement side as Perl code to evaluate, so you can compute the replacement. /r returns the modified copy and leaves the original untouched instead of editing in place.
/e is the one to highlight: it turns a substitution into a mini transform, letting you run a function on each match.
my $s = "a1 b2 c3";
(my $copy = $s) =~ s/\d/X/gr; # /r: $s unchanged, $copy = "aX bX cX"
my $doubled = $s =~ s/(\d)/$1 * 2/ger; # /e + /r: "a2 b4 c6"Key point: The /r modifier (non-destructive substitution, Perl 5.14+) is a nice modern detail that signals you keep up with the language.
A reference is a scalar that points to another value: an array, a hash, another scalar, or a subroutine. You create one with the backslash operator and dereference it with a sigil to get the original value back.
References matter because Perl's arrays and hashes flatten when combined, so you can't nest them directly. A reference is a single scalar, so an array of array references gives you real nested data structures and lets you pass containers to subroutines without copying.
my @data = (1, 2, 3);
my $ref = \@data; # reference to the array
print @$ref; # dereference: 123
print ${$ref}[0]; # element access: 1
print $ref->[0]; # arrow form: 1Key point: The key insight the question needs: arrays flatten, so nesting requires references. If you can say that, the rest follows.
Watch a deeper explanation
Video: Perl Basics #7: References [Part 1] (PerlTechStack, YouTube)
Square brackets create an anonymous array reference, and curly braces create an anonymous hash reference. You get a scalar reference directly, without naming an intermediate variable.
This is how you build nested structures: an array of hash references, a hash of array references, and so on. The arrow operator walks down the levels.
my $people = [
{ name => "Asha", role => "eng" },
{ name => "Ben", role => "ops" },
];
print $people->[0]{name}; # Asha
print $people->[1]{role}; # opsPut the right sigil in front of the reference: @$aref for the whole array, %$href for the whole hash, and $$sref for a scalar. For single elements use the arrow: $aref->[0] and $href->{key}. To get an array reference's length, use scalar @$aref.
The arrow can be omitted between subscripts, so $data->[0][1] and $data->[0]->[1] mean the same thing.
my $aref = [10, 20, 30];
my $href = { a => 1, b => 2 };
print "@$aref"; # 10 20 30
print $aref->[1]; # 20
print scalar @$aref; # 3
print join ",", keys %$href; # a,bArguments arrive flattened in the special array @_. You unpack them by assigning to a list, commonly my ($x, $y) = @_; at the top of the sub, or by shifting them off one at a time.
Because everything flattens into @_, passing arrays or hashes by value loses their boundaries; pass references instead so the sub knows where one container ends and the next begins.
sub area {
my ($width, $height) = @_;
return $width * $height;
}
print area(3, 4); # 12
# pass a container by reference
sub total { my $ref = shift; my $s = 0; $s += $_ for @$ref; $s }
print total([1, 2, 3]); # 6Key point: The follow-up: 'why pass an array by reference?'. Because @_ flattens, so two arrays passed by value merge into one indistinguishable list.
A sub returns the value of its last expression unless you use an explicit return. What the caller gets can depend on context: you can check wantarray inside the sub to see whether it was called in list or scalar context and return accordingly.
A common pattern is returning a list in list context and a count or single value in scalar context, mirroring how built-ins like the match operator behave.
sub results {
my @found = (1, 2, 3);
return wantarray ? @found : scalar @found;
}
my @all = results(); # (1, 2, 3)
my $n = results(); # 3map transforms each element of a list and returns a new list, grep filters a list keeping elements where the block is true, and sort orders a list, by default as strings. All three set $_ to the current element inside their block.
sort takes an optional comparison block using $a and $b, so sort { $a <=> $b } sorts numerically. Chaining map, grep, and sort is idiomatic Perl for list pipelines.
my @nums = (3, 1, 4, 1, 5);
my @evens = grep { $_ % 2 == 0 } @nums; # (4)
my @sqrs = map { $_ * $_ } @nums; # (9, 1, 16, 1, 25)
my @sorted = sort { $a <=> $b } @nums; # (1, 1, 3, 4, 5)Key point: The default string sort is a classic trap: sort (10, 9, 100) gives (10, 100, 9). Reaching for { $a <=> $b } shows you know it.
By default sort compares elements as strings, so it orders them lexically: "10" comes before "2" because "1" is less than "2" character by character. That gives (10, 2, 33) as (10, 2, 33) ordered stringwise, not the numeric order you expect.
The fix is a numeric comparator: sort { $a <=> $b } @list. Use the spaceship operator <=> for numbers and cmp for strings.
my @n = (10, 2, 33, 4);
my @str = sort @n; # (10, 2, 33, 4) string order
my @num = sort { $a <=> $b } @n; # (2, 4, 10, 33) numeric
my @rev = sort { $b <=> $a } @n; # (33, 10, 4, 2) descendingwantarray returns true if the current subroutine was called in list context, false (but defined) if in scalar context, and undef if called in void context. It lets one sub adapt its return value to how it was called.
Use it sparingly; a sub that returns wildly different shapes depending on context can confuse callers. It's most justified when mirroring built-in behavior.
sub context_check {
return wantarray ? "list" : defined(wantarray) ? "scalar" : "void";
}
my @x = context_check(); # "list"
my $y = context_check(); # "scalar"
context_check(); # voiduse loads a module at compile time and runs its import routine, bringing names into your namespace. require loads at runtime and does not import anything by itself. use Foo; is roughly a compile-time require Foo; plus Foo->import;.
Reach for use for normal module loading, and require when you want to defer loading until runtime, for example loading an optional dependency only if a feature is enabled.
use List::Util qw(sum); # compile time, imports sum
print sum(1, 2, 3); # 6
# runtime, conditional load
if ($want_json) {
require JSON::PP;
JSON::PP->import;
}CPAN is the Perl Archive Network, the central repository of open-source Perl modules and one of the language's oldest strengths: a large library of reusable code for everything from date handling to web frameworks.
You install modules with a client like cpan or cpanm, then load them with use. In real projects you pin versions and often use a local library setup (local::lib or a container) so installs are reproducible.
# install a module with cpanminus
cpanm DateTime
# then in your script
# use DateTime;
# my $now = DateTime->now;A slice reads or writes several elements at once. An array slice uses @ with a list of indices, and a hash slice uses @ with a list of keys, returning the matching values as a list.
Slices are compact for pulling a subset of a hash into variables or building a smaller hash. Since Perl 5.20 you can also take a key-value hash slice with the % sigil.
my %user = (name => "Asha", role => "eng", team => "core");
my ($name, $role) = @user{qw(name role)}; # hash slice
my @nums = (10, 20, 30, 40);
my @some = @nums[0, 2]; # array slice: (10, 30)The x operator repeats: a string times a number repeats the string, and a list in parentheses times a number repeats the list. The range operator .. generates a sequence, numeric (1..5) or alphabetic (a..z), commonly used in for loops and slices.
In scalar context .. also works as a flip-flop operator for line-range selection, a niche feature worth naming but rarely written.
print "-" x 10; # ----------
my @zeros = (0) x 5; # (0, 0, 0, 0, 0)
my @digits = (1..5); # (1, 2, 3, 4, 5)
my @letters = (a..e); # (a, b, c, d, e)A heredoc is a multi-line string literal introduced with <<"LABEL" or <<'LABEL'. Everything up to a line containing only the label becomes the string. Double-quote-style interpolates variables; single-quote-style is literal.
Use heredocs for templates, SQL, HTML, or any block of text where line breaks matter. The indented form <<~LABEL (Perl 5.26+) strips leading whitespace so the closing label can be indented.
my $name = "Asha";
my $msg = <<"END";
Hi $name,
Your report is ready.
END
print $msg;The ternary operator condition ? if_true : if_false chooses between two expressions. It reads well for a single decision and is fine chained for a few cases, but deeply nested ternaries become unreadable fast.
When a chain grows past two or three branches, a set of if/elsif blocks or a dispatch hash is clearer. Interviewers notice when readable structure loses to cleverness.
my $score = 82;
my $grade = $score >= 90 ? "A"
: $score >= 80 ? "B"
: $score >= 70 ? "C"
: "F";
print $grade; # BThe classic mechanism is eval { ... }; which traps a die inside the block; after it, $@ holds the error message (empty on success). You check $@ to detect and handle the failure.
That pattern has subtle traps ($@ can be clobbered), so modern code uses Try::Tiny or the built-in try/catch feature to make the intent clear and the error handling reliable.
my $result = eval {
risky_operation();
1;
};
if (!$result) {
warn "failed: $@";
}
# modern style
# use Try::Tiny;
# try { risky_operation() } catch { warn "failed: $_" };The eval error-handling pattern
Copy $@ into a local variable before doing anything else, because later calls can clobber it.
Key point: Naming both eval/$@ and Try::Tiny (or the newer try/catch feature) shows you know the history and the current best practice.
sprintf builds a formatted string from a template and a list of values, using the same conversion codes as C: %d for integers, %f for floats with a precision like %.2f, %s for strings, and width and padding flags in between.
It's the tool for aligned columns, fixed decimals, zero-padding, and hex or binary output. printf does the same but prints directly.
my $price = sprintf "%.2f", 3.1; # "3.10"
my $padded = sprintf "%05d", 42; # "00042"
my $row = sprintf "%-10s %5d", "apples", 3; # left, then right alignedThe built-in localtime and gmtime turn an epoch timestamp into date parts, but their return list is easy to misuse (the month is 0-based and the year is offset from 1900). For real work most teams use the DateTime module from CPAN, which handles arithmetic, time zones, and formatting correctly.
The honest interview coverage names both: localtime for a quick timestamp, DateTime when correctness across time zones matters.
my ($sec, $min, $hour, $mday, $mon, $year) = localtime;
printf "%04d-%02d-%02d\n", $year + 1900, $mon + 1, $mday;
# note: $mon is 0-based, $year is years since 1900Key point: Mentioning the 0-based month and 1900-offset year gotchas, then reaching for DateTime, is exactly the awareness this question screens for.
advanced rounds probe internals, OO design, and production scars. Expect every answer here to draw a follow-up.
Classic Perl OO is built from three pieces: a package is a class, a blessed reference (usually a hash reference) is an object, and a subroutine in the package is a method that receives the object as its first argument. bless ties a reference to a package so method calls dispatch to that package.
It's more manual than most languages, which is why modern code reaches for Moose or Moo to get attributes, accessors, and type checks declaratively. Knowing the raw mechanism and the modern tooling is The production-ready answer.
package Account;
sub new {
my ($class, %args) = @_;
my $self = { balance => $args{balance} // 0 };
return bless $self, $class;
}
sub deposit {
my ($self, $amount) = @_;
$self->{balance} += $amount;
}
my $a = Account->new(balance => 100);
$a->deposit(50);Key point: The follow-up is 'what does bless actually do?'. Answer: it tags a reference with a package so Perl knows which methods to dispatch.
bless associates a reference with a package (class), so calling a method on it, like $obj->method, looks up method in that package and passes $obj as the first argument. Before bless, the reference is just data; after, it's an object.
You can ask an object what it is with ref($obj), which returns the package name for a blessed reference. That's how isa and can checks and duck-typing work under the hood.
my $data = { x => 1 };
print ref $data; # HASH
bless $data, "Point";
print ref $data; # Point
print $data->isa("Point") ? "yes" : "no"; # yesMoose is a full object system for Perl that adds declarative attributes, type constraints, roles, method modifiers, and more, at the cost of a slower startup. Moo is a lighter subset with most of the everyday features and much faster load time, and it upgrades to Moose transparently if needed.
Reach for Moo in small tools and short-lived processes where startup time matters, and Moose in larger applications that use its deeper features. Both remove the hand-rolled constructor and accessor boilerplate of classic Perl OO.
| Moose | Moo | |
|---|---|---|
| Feature set | Full: types, roles, meta-object protocol | Core subset of Moose |
| Startup cost | Higher | Low |
| Best for | Large, long-running apps | CLIs, libraries, fast startup |
Inheritance sets a package's @ISA (or use parent/use base) so method lookup falls through to parent classes. Perl supports multiple inheritance, with method resolution walking @ISA depth-first by default, or C3 order if you enable it.
Roles (via Moose or Moo) compose behavior into a class without an is-a relationship, which sidesteps the diamond problems of deep multiple inheritance. The modern guidance is composition through roles over deep inheritance trees.
package Animal;
sub speak { my $self = shift; print $self->sound, "\n" }
package Dog;
use parent -norequire, "Animal";
sub sound { "woof" }
Dog->speak; # woofA closure is an anonymous subroutine that captures lexical variables from the scope where it was defined, keeping them alive after that scope exits. Each closure gets its own copy of the captured variables.
Closures power counters, callbacks, iterators, and private state. A factory that returns a sub with a captured my variable is the canonical example, and it's how you get encapsulated state without a full class.
sub make_counter {
my $count = shift // 0;
return sub { return $count++ }; # captures $count
}
my $next = make_counter(10);
print $next->(); # 10
print $next->(); # 11Key point: The gotcha follow-up: two counters from make_counter are independent because each call creates a fresh my $count. Say that and you're done.
Each package has a symbol table, a hash mapping names to typeglobs. A typeglob (written *name) is the collection of everything a name can refer to at once: the scalar, array, hash, code, and filehandle slots for that name.
Aliasing a typeglob makes two names refer to the same underlying data, which is how older code shared filehandles or created aliases before references were common. Modern code rarely touches globs directly, but understanding them explains how symbolic references and Exporter work.
our $value = 1;
our @value = (1, 2, 3);
*alias = *value; # alias every slot of "value"
print $alias; # 1
print "@alias"; # 1 2 3Perl uses reference counting: every value tracks how many references point to it, and when that count drops to zero the memory is freed. This is deterministic, so objects clean up as soon as the last reference goes away.
The weakness is circular references: two structures pointing at each other keep each other's count above zero and never free. The fix is Scalar::Util::weaken to make one side a weak reference that doesn't raise the count.
use Scalar::Util qw(weaken);
my $parent = {};
my $child = { parent => $parent };
$parent->{child} = $child;
weaken $child->{parent}; # break the cycle so both can freeKey point: Reference-counting plus the circular-reference leak plus weaken is the full arc. Naming Scalar::Util::weaken is the detail that lands.
Symptoms are steady memory growth in a long-running process with no obvious unbounded cache. Because reference counting can't free a cycle, structures like parent-child trees or observer lists that point both ways leak until the process exits.
Find them by reasoning about which structures reference each other, or with tools like Devel::Cycle and Devel::Leak. Fix by weakening the back-reference (the child's pointer to its parent) so only the strong direction keeps the object alive.
Taint mode, enabled with the -T flag, marks all data from outside the program (command-line arguments, environment, file input, network) as tainted and refuses to use it in operations that affect the system, like running a shell command or opening a file for writing, until you explicitly untaint it.
You untaint by extracting the safe part through a regex capture. It's a defense against injection in setuid scripts and any program handling untrusted input, so naming it signals security awareness.
#!/usr/bin/perl -T
use strict; use warnings;
my $input = $ARGV[0];
# untaint by capturing only the allowed shape
if ($input =~ /^([\w.-]+)$/) {
my $safe = $1; # now untainted
}Data::Dumper serializes any nested Perl data structure into readable, re-parseable text, so you can print exactly what a reference contains. It's the first tool most Perl developers reach for when a structure isn't what they expect.
For interactive debugging, the built-in debugger (perl -d) lets you step, set breakpoints, and inspect variables. Between Data::Dumper for structure and perl -d for control flow, most bugs get cornered quickly.
use Data::Dumper;
$Data::Dumper::Sortkeys = 1; # stable key order
my $data = { users => ["Asha", "Ben"], count => 2 };
print Dumper($data);Watch a deeper explanation
Video: Perl Tutorial 101 - Data::Dumper - Print Data Structures (ScriptSocket Video, YouTube)
A hard reference is a real pointer to a value: it keeps that value alive and is what \@array or [ ... ] produces. A symbolic reference is a string used as a variable name at runtime, so $$name looks up the package variable whose name is in $name.
Symbolic references are dangerous and blocked by use strict 'refs' because they let arbitrary strings poke at globals. Always prefer hard references; that's the whole point of the strict pragma flagging them.
no strict "refs";
our $color = "red";
my $name = "color";
print ${$name}; # "red" symbolic reference (avoid)
use strict "refs";
my $ref = \$color; # hard reference (preferred)
print $$ref; # "red"Key point: The clean answer: hard references are pointers, symbolic references are variable names by string, and use strict 'refs' exists to stop the latter.
Watch a deeper explanation
Video: Perl Tutorial 77 - Hard References (ScriptSocket Video, YouTube)
Prototypes are an older, easily misunderstood feature that hints at how a sub should parse its arguments (for example forcing a block or a single array). They affect parsing, not validation, so they surprise people and are best avoided for normal code.
Signatures (stable since Perl 5.36) are the modern feature: sub add ($x, $y) { $x + $y } declares named parameters directly, with defaults and a slurpy final parameter. Use signatures for readable argument handling and reserve prototypes for the rare cases that need them.
use v5.36; # signatures on by default
sub greet ($name, $greeting = "Hi") {
return "$greeting, $name";
}
print greet("Asha"); # Hi, Asha
print greet("Ben", "Hello"); # Hello, Benbinds an ordinary variable (scalar, array, hash, or filehandle) maps to a class so that every access to that variable runs a method behind the scenes. Reading a tied hash key can hit a database, and storing to a tied scalar can validate or log.
It's how modules like DB_File present an on-disk store as a normal hash. It's a specialty tool: clever, occasionally the right answer, but a maintenance surprise if overused, so name it and note its cost.
Measure before changing anything: Devel::NYTProf is the standard profiler and shows time per line and per subroutine against a real workload. Then fix in order of impact: better algorithms and data structures, avoiding needless copies of large arrays and hashes, precompiling regexes with qr//, and reducing I/O by batching.
The discipline the question needs is profile-first: assumptions about Perl bottlenecks are often wrong, and NYTProf points at the real hot spot.
# precompile a regex used in a hot loop
my $pattern = qr/^\d{4}-\d{2}-\d{2}/;
for my $line (@lines) {
next unless $line =~ $pattern; # no recompile per iteration
process($line);
}qr// compiles a regular expression once into a reusable pattern object you can store in a variable and use later with =~. Without it, a pattern built from a variable can be recompiled on every match inside a loop.
Precompiling with qr// removes that repeated compilation and lets you build patterns from parts, combine them, and pass them around like any other value. In a hot loop the saving is real and measurable.
my $word = qr/\w+/;
my $email = qr/$word\@$word\.$word/; # compose patterns
for my $line (@lines) {
print "$line\n" if $line =~ $email;
}Perl distinguishes bytes from characters. To work with text correctly you decode input bytes into Perl's internal character strings and encode again on output, usually with the Encode module or by setting the encoding on a filehandle. use utf8; tells Perl your source file itself is UTF-8.
The classic bug is treating UTF-8 bytes as characters, which breaks length and regex on multibyte text. The rule to state: decode on input, work in characters, encode on output.
use utf8; # this source file is UTF-8
use open ":std", ":encoding(UTF-8)";
my $name = "Renée";
print length $name; # 5 characters, not the byte countKey point: The one-line answer the question needs: decode on the way in, encode on the way out, work in characters in between.
Perl's testing is built on Test::More, which gives functions like ok, is, and is_deeply that emit TAP (Test Anything Protocol) output. Test files live in a t/ directory and run with prove, which summarizes pass and fail counts.
For larger suites, Test2 is the modern framework, and modules like Test::MockModule handle mocking. A strong answer mentions testing behavior, using is_deeply for structures, and running the suite in CI.
use Test::More tests => 2;
use_ok("Billing");
my @got = Billing::split_amount(100, 3);
is_deeply(\@got, [34, 33, 33], "splits evenly with remainder");A dispatch table is a hash mapping keys to code references, so you look up and call the right subroutine by key instead of walking a long if/elsif chain. It's faster to read, easy to extend, and turns control flow into data.
It shines for command handlers, state machines, and plugin-style routing. Adding a new case means adding a hash entry, not editing a branch, which keeps the logic flat.
my %handler = (
add => sub { $_[0] + $_[1] },
sub => sub { $_[0] - $_[1] },
mul => sub { $_[0] * $_[1] },
);
my $op = "mul";
print $handler{$op}->(6, 7); # 42Key point: Reaching for a dispatch table when asked to route commands indicates experienced Perl; a giant if/elsif chain indicates junior.
Raku, originally announced as Perl 6, turned into a separate language with its own design and runtime rather than a new version of Perl 5. It was renamed Raku in 2019 to make the split clear. Perl 5 kept developing independently and is the Perl in production today.
So the correct framing in an interview is that they're two different languages that share heritage, not old and new versions of one. Answer Perl questions for Perl 5 unless the interviewer says Raku.
Start by reproducing or observing: check the exact Perl version and installed module versions, since a CPAN version drift is a frequent cause, then read the logs and any die or warn output around the failure. Confirm use strict and use warnings are on, because a silent undef or an uninitialized value is a common culprit.
For a leak, watch memory over time and look for unbounded structures or circular references, using Devel::NYTProf for CPU and Data::Dumper or Devel::Cycle for state. Then fix at the right layer and confirm with a measurement. The methodology, observe, hypothesize, verify, fix, confirm, is what matters.
A production debugging sequence
The sequence out loud. the method more than any single module you name is the technical point.
Key point: The methodology is; the specific modules are supporting evidence.
Perl wins when the job is text: parsing logs, transforming files, and stitching command-line tools together with regular expressions baked into the syntax. It trades a clean, one-obvious-way style for TMTOWTDI (there's more than one way to do it), which makes quick scripts fast to write but large codebases harder to keep consistent. Python took much of Perl's general-scripting ground with more readable syntax, Bash stays lighter for simple shell glue, and Raku (formerly Perl 6) is a separate, redesigned language rather than a newer Perl 5. Saying these trade-offs out loud is itself an interview signal.
| Language | Best at | Typing | Watch out for |
|---|---|---|---|
| Perl | Text processing, regex, sysadmin glue | Dynamic | Readability at scale, sigil rules |
| Python | General scripting, data, ML | Dynamic | Weaker one-liner text ergonomics |
| Bash | Simple shell glue, piping commands | Untyped strings | Fragile past a few dozen lines |
| Raku (Perl 6) | Modern redesign, gradual typing | Gradual | Separate language, smaller ecosystem |
Prepare in layers, and practice out loud. Most Perl rounds move from concept questions to live scripting to a debugging or regex discussion, so rehearse each stage rather than only reading answers.
The typical Perl interview flow
Earlier rounds increasingly run as AI coding interviews. The concepts on this page are what gets probed at every stage.
10 questions, about 6 minutes. Score 70% or higher to earn a shareable certificate.
Hyring builds the AI Coding Interviewer that runs live technical rounds in 20+ languages for 5,000+ hiring teams. These Perl questions and scoring notes reflect what actually gets asked and evaluated inside the interviews we host.
See how the AI Coding Interviewer works