Perl Interview Questions and Answers

Last updated:

Check out 40 of the most common Perl interview questions, then take an AI-powered practice interview

System AdministrationWeb DevelopmentBioinformaticsText ProcessingCGI
40+
Questions
15
Basic
16
Intermediate
9
Advanced
Q1

What are the three fundamental data types in Perl?

BasicFundamentals

Answer

Perl has three core variable types, each with its own sigil that tells you what you're looking at: scalars ($), arrays (@), and hashes (%). A scalar holds a single value, number, string, or reference. An array is an ordered list of scalars indexed by integers.

A hash is an unordered set of key-value pairs where keys are strings and values are scalars. The sigils are not just decoration: they're part of the variable's identity, which is why `$name`, `@name`, and `%name` are three completely independent variables. When you access an array element you use `$array[0]` (scalar context, scalar sigil), and a hash element is `$hash{key}`, the sigil follows the value you're getting back, not the container.

A fourth thing interviewers probe: `undef` is not a separate type, it is the absence of a value that any scalar can hold, and `defined()` is how you test for it. Internally a scalar keeps separate integer (IV), floating point (NV) and string (PV) slots and converts between them silently, so `my $x = '10abc'; my $y = $x + 5;` gives 15 plus the warning `Argument '10abc' isn't numeric in addition`. Arrays know their last index as `$#arr`, which is one less than `scalar @arr`, and assigning `$#arr = -1` truncates the array in place while keeping the allocated buffer.

Hash key order has been randomised per process since Perl 5.18, so any code or test that depends on `keys %h` coming back in a fixed order will pass locally and fail in CI. The usual follow-up is 'what else can a scalar hold?', and the answer is references, including code refs and globs, which is exactly what `ref()` and `Scalar::Util::reftype` exist to tell apart.

my $name = 'Priya';                    # scalar
my @scores = (85, 92, 78);              # array
my %ages = (alice => 30, bob => 25);    # hash

print $name;            # Priya
print $scores[0];       # 85   (single element → scalar sigil)
print $ages{alice};     # 30   (single element → scalar sigil)
print scalar @scores;   # 3    (array in scalar context = length)

Key Points

  • Sigils tell you the type of value being accessed, not the variable
  • $ for single value, @ for list, % for key-value pairs
  • Same name with different sigils = different variables
Q2

What is context in Perl and why does it matter?

BasicContext

Answer

Context in Perl is the most-failed interview question because no other mainstream language has anything like it. Every expression in Perl is evaluated in either scalar context or list context, and many operators return different values depending on which one they're in. The classic example: `my @arr = (1,2,3); my $count = @arr;`, putting `@arr` on the right of a scalar assignment forces scalar context, so you get the length (3) rather than the list.

In list context you'd get the list itself. The `scalar` keyword forces scalar context explicitly. This is also why `localtime()` returns a string in scalar context and a 9-element list in list context.

Understanding context is the difference between writing correct Perl and writing Perl that surprises you at 3 a.m. Two follow-ups separate juniors from seniors. First, `my ($x) = f()` and `my $x = f()` call `f` in different contexts, so a function ending in `return @results;` hands you the first element in one case and the count in the other, which is the single most common context bug in real codebases.

Second, `return;` gives an empty list in list context and `undef` in scalar context, whereas `return undef;` gives a one-element list in list context, so `if (my @r = f())` is true even on failure. Other operators worth naming: `reverse` reverses a list in list context but reverses a concatenated string in scalar context, a hash in scalar context has returned a plain key count since Perl 5.26 (older Perls returned a bucket-usage string like '3/8'), and `sort` in scalar context has no defined behaviour at all. Boolean is just a flavour of scalar context, while `print`, `join` and subroutine argument lists all impose list context.

my @arr = (10, 20, 30);

my $len   = @arr;            # 3, scalar context
my ($a)   = @arr;            # 10, list context, takes first
my $first = $arr[0];         # 10, direct index
print 'count: ', scalar @arr; # forced scalar context

# localtime is the textbook example:
my $now  = localtime;        # 'Tue May 12 14:30:21 2026'
my @parts = localtime;       # (21, 30, 14, 12, 4, 126, 2, 132, 0)
💡 Pro Tip: When unsure, use `scalar()` to force scalar context, it's cheaper than a bug.
Q3

What does `use strict; use warnings;` do and why should you always use them?

BasicBest Practices

Answer

These two pragmas are the closest thing Perl has to a 'modern mode'. `use strict` forbids three things that legacy Perl allowed: unqualified bareword identifiers (catches typos like `prnt` vs `print`), symbolic references (using a string as a variable name), and undeclared variables (you must declare with `my`, `our`, or `state`). `use warnings` enables compile-time and runtime warnings about likely bugs: using undefined values, deprecated syntax, suspicious type conversions. In Perl 5.36+ you can replace both with `use v5.36;` which enables strict, warnings, and modern features (signatures, say) in one line. Any Perl code written without these in 2026 is a red flag, either it's pre-2000 legacy code or the author hasn't kept up.

Two details interviewers push on. `strict` is really three sub-pragmas (`vars`, `refs`, `subs`) and you can relax one for a block with `no strict 'refs';`, which is what you need when you deliberately poke the symbol table to install a method at runtime. `warnings` is lexical, whereas the old `-w` switch is global and turns warnings on inside every CPAN module you load, which is why `-w` in a shebang produces noise nobody can act on. You can promote warnings to exceptions with `use warnings FATAL => 'all';`, which is reasonable in an application and hostile in a library because you change the failure mode of somebody else's program. Know what these pragmas do not do as well: no type checking, no protection against `undef` flowing through your code, no taint checks. From 5.36 the version bundle also disables indirect object syntax (`new Foo(...)`) and multidimensional array emulation, so legacy code can fail to compile under `use v5.36;` even though it ran unchanged for twenty years.

# Old style:
use strict;
use warnings;

# Modern (Perl 5.36+):
use v5.36;   # implies strict, warnings, signatures, say, etc.

sub greet ($name) {
    say "Hello, $name";
}

greet('Aarav');
Q4

How do you read a file line by line in Perl?

BasicFile I/O

Answer

The idiomatic approach uses a three-argument `open` (safer because it separates the mode from the filename), a `while` loop over the file handle, and `chomp` to strip the trailing newline. Always check the return value of `open` and die with an error message, file I/O failures are silent otherwise. For modern Perl, use lexical file handles (`my $fh`) rather than the old bareword form (`FH`) because they auto-close when they go out of scope and don't pollute the global namespace.

Details that show up in follow-ups: `$.` holds the current line number, `$/` is the input record separator, and `local $/;` inside a block puts the handle in slurp mode so `my $content = <$fh>;` reads the entire file (fine for a config file, a bad idea for a 10 GB log). Setting `$/ = ''` switches to paragraph mode. The `while (my $line = <$fh>)` form is special-cased by the compiler to wrap the assignment in `defined()`, so a line containing only '0' does not silently end the loop, and that magic does not extend to hand-rolled conditions.

Pass `'<:encoding(UTF-8)'` as the mode when the file is text in any non-ASCII encoding, otherwise you get raw bytes and `length` lies to you. `$!` is only meaningful immediately after a failed syscall, so build the die message on the same statement. For write handles check the return value of `close` as well as `open`, because buffered data is flushed at close and that is exactly where a full disk surfaces. Modern code often replaces the whole dance with `Path::Tiny` or the `autodie` pragma.

use v5.36;

open(my $fh, '<', 'access.log') or die "can't open log: $!";
while (my $line = <$fh>) {
    chomp $line;
    next if $line =~ /^\s*$/;       # skip blank lines
    next if $line =~ /^#/;          # skip comments
    say "got: $line";
}
close $fh;

Key Points

  • 3-arg open is safer than 2-arg
  • chomp removes the newline
  • Check `$!` for the OS error string
Q5

What is `$_` (the default variable) and where does it appear?

BasicDefaults

Answer

`$_` is Perl's default scalar variable. Many built-ins read from or write to `$_` when you don't pass an explicit argument: `print`, `chomp`, regex matches (`if (/pattern/)`), `chop`, `lc`, `uc`, and most notably the `for` loop. Inside `for my $x (@arr)` you'd use `$x`; inside `for (@arr)` the current element is in `$_`.

The hidden default `$_` is a major reason Perl can be terse, and a major reason it can be hard to read. Modern style prefers explicit loop variables for anything longer than a one-liner. The corresponding hidden default for arrays is `@_` (the function's argument list).

What makes `$_` dangerous is that it is a package global (`$main::_`), not a lexical, so two pieces of code that both use it without protection clobber each other: a sub called from inside `for (@list)` that itself runs `while (<$fh>)` overwrites the caller's loop variable. A `for` loop implicitly localises `$_` for you, but a bare `while (<$fh>)` does not, which is why careful library code writes `local $_;` first. The experimental lexical `my $_` added in 5.10 was removed in 5.24, so do not reach for it.

The subtler trap is aliasing: in `map`, `grep` and `for`, `$_` is an alias to the real element rather than a copy, so `for (@names) { s/^\s+// }` edits `@names` in place, which is either the neatest trick in the language or an hour of debugging. Sorting is the exception that proves the rule, since `sort` blocks use the package globals `$a` and `$b` instead. A common interview exercise is rewriting a `$_`-heavy one-liner with named variables, and that is the right instinct for anything that has to be maintained.

my @names = ('alice', 'bob', 'priya');

# Implicit $_:
for (@names) {
    print uc, "\n";          # uses $_, prints ALICE, BOB, PRIYA
}

# Explicit (preferred for non-trivial code):
for my $name (@names) {
    print uc($name), "\n";
}

# Implicit $_ in regex:
my @errors = grep { /ERROR/ } @log_lines;
Q6

How do you do basic regex matching and substitution in Perl?

BasicRegex

Answer

Regex is Perl's signature feature, it's built into the language rather than living in a library. Match with `=~ m/pattern/` (the `m` is optional with `/`), substitute with `=~ s/pattern/replacement/`, and translate with `=~ tr/from/to/`. The match returns true/false in scalar context, or the captured groups in list context.

Modifiers go after the closing delimiter: `i` (case insensitive), `g` (global, match all), `s` (dot matches newline), `m` (multi-line, `^`/`$` match each line), `x` (extended, whitespace and comments allowed). Capture groups are available as `$1`, `$2`, etc. after a successful match. Perl's regex engine is the reference implementation that PCRE (and through it, most other languages) copied.

A senior interviewer follows up in three places. Failure semantics first: a failed match leaves `$1` holding the value from the previous successful match, so always guard captures with `if ($str =~ /.../) { ... }` instead of reading `$1` unconditionally, because the stale-capture bug is silent. Next, `qr//` compiles a pattern into a first-class object you can store in a variable, pass to a function, and interpolate into a bigger pattern, which is both faster inside a loop and the only sane way to build patterns dynamically.

Third, `/g` changes behaviour with context: in list context it returns all matches, in scalar context it advances one match per call using the offset stored in `pos($str)`, which is how you write a tokeniser together with `\G`. `tr///` returns the count of characters translated, so `my $commas = ($line =~ tr/,//);` is the cheapest way to count a character. Named captures land in `%+`, with `%-` holding every value when a name repeats. Historically `$&` slowed down every regex in the program, and although modern Perls removed that penalty, named captures remain the maintainable choice.

my $log = 'ERROR 2026-05-12 user=priya code=401';

if ($log =~ /user=(\w+)\s+code=(\d+)/) {
    say "user: $1, code: $2";    # user: priya, code: 401
}

# Substitute (returns count of replacements):
my $text = 'foo bar foo';
my $count = ($text =~ s/foo/baz/g);   # $text = 'baz bar baz', $count = 2

# Case-insensitive, with named captures (preferred over $1, $2):
if ($log =~ /user=(?<user>\w+)/i) {
    say $+{user};                # priya
}
💡 Pro Tip: Use named captures `(?<name>...)` and `%+` instead of `$1`/`$2` in any regex over a few groups, much easier to maintain.
Q7

What is the difference between `my`, `our`, and `local`?

BasicScoping

Answer

Three different scoping mechanisms with very different semantics. `my` creates a lexically-scoped variable, visible only inside the enclosing block, the most common and safest choice. `our` creates a package-scoped global, visible across the entire package, mostly used for module-level constants and configuration. `local` is the weird one: it doesn't create a new variable, it saves the existing package variable's value, lets you change it temporarily, and restores it when the block exits. `local` is what you need when monkey-patching special variables like `$/` (the input record separator). Modern code uses `my` for almost everything. Interview-wise, the common trap is using `our` when `my` is meant, exposing internals you didn't intend to.

Two things complete the answer. There is a fourth declaration, `state` (Perl 5.10), which creates a lexical initialised once that keeps its value between calls, the clean way to write a counter or a lazily built lookup table inside a sub. And `local` only works on package variables: `local $x` on a lexical dies with `Can't localize lexical variable $x`.

What makes `local` genuinely useful is dynamic scope, meaning every function called from inside the block also sees the new value. That is why the production idioms are `local $/;` for slurp mode, `local $| = 1;` for autoflush, `local $ENV{PATH} = '/usr/bin';` before a `system` call, and `local $SIG{ALRM}` around a timeout. You can also localise a single hash or array element, something `my` cannot do at all. On `our`: it is not a global declaration but a lexically scoped alias to `$Package::name`, so an `our $VERSION` stays visible further down the same file even after a `package` switch, which surprises people who expect it to track the current package.

package Logger;
our $LEVEL = 'INFO';   # package global, accessible as $Logger::LEVEL

sub log_msg {
    my $msg = shift;                  # lexical, just for this sub
    local $\ = "\n";                  # temporarily set output record sep
    print "[$LEVEL] $msg";
    # $\ is restored when sub ends
}
Q8

How do you define and call a subroutine with arguments?

BasicSubroutines

Answer

Classic Perl receives arguments in the `@_` array, you unpack with `shift` or list assignment. Modern Perl (5.20+, stable since 5.36) supports native signatures, which look like Python or JavaScript parameters. Signatures also validate arity, calling with the wrong number of arguments dies.

For new code in 2026, use signatures; for reading old code, remember `@_` is the implicit list of all arguments. Three follow-ups are standard. First, `@_` aliases the caller's variables, so `sub bump { $_[0]++ }` really does increment the variable that was passed in, which is how `chomp` behaves and a live footgun in legacy code.

Second, `sub foo ($$)` in a pre-2020 codebase is a prototype, not a signature: prototypes change how the parser treats a call site, signatures bind arguments at runtime. Once signatures are enabled, parentheses after the sub name are parsed as a signature and a genuine prototype has to move to the `:prototype($$)` attribute. Third, arity is enforced, so calling a two-parameter sub with three arguments dies with `Too many arguments for subroutine` and omitting a parameter that has no default dies with `Too few arguments`.

Defaults are evaluated at call time and can reference earlier parameters, so `sub f ($x, $y = $x * 2)` works. A slurpy `@rest` or `%opts` must come last, and `%opts` requires an even number of remaining arguments. Signatures were experimental from 5.20 and stable from 5.36, so on a RHEL box still running 5.32 you need `use feature 'signatures'; no warnings 'experimental::signatures';` first.

use v5.36;

# Modern (signatures):
sub area ($length, $width) {
    return $length * $width;
}

# Old style, still ubiquitous in legacy code:
sub area_old {
    my ($length, $width) = @_;
    return $length * $width;
}

# With defaults and slurpy:
sub greet ($name, $greeting = 'Hello', @extras) {
    return "$greeting, $name" . (@extras ? " (" . join(',', @extras) . ")" : '');
}

say greet('Aarav');                         # Hello, Aarav
say greet('Aarav', 'Namaste', 'sir');       # Namaste, Aarav (sir)

Key Points

  • @_ holds all args in classic style
  • Signatures (Perl 5.36+) are the modern way
  • @_ is aliased, modifying $_[0] modifies the caller's variable
Q9

How do you check if a key exists in a hash?

BasicHashes

Answer

Use the `exists` function. The common trap: `if ($hash{key}) { ... }` is true only when the value is also truthy, so it misses keys with values of 0, empty string, or undef. `exists` checks only whether the key is in the hash, regardless of value. For 'has a value AND it's defined', use `defined($hash{key})`.

For 'key exists' use `exists`. For 'value is truthy' use the plain boolean, but be sure that's what you mean. The nested case is where people get caught: `exists $h->{users}{42}` autovivifies `$h->{users}` as an empty hash ref even though you only asked a question, because only the final level is exempt.

In a long-running process, a lookup loop over unknown IDs therefore grows memory forever. Check level by level, or switch the behaviour off for the scope with `no autovivification;`. `delete` removes the key entirely and returns the deleted value, which is not the same as `$h{k} = undef`: after the assignment `exists` is still true, after `delete` it is false, and that difference shows up the moment the hash is serialised to JSON or used to build an SQL update. `delete @h{qw(a b c)}` deletes a whole slice in one statement, while `delete` on array elements is deprecated and should be `splice`. For defaults, reach for the defined-or operator `//` added in Perl 5.10 (`my $timeout = $config{timeout} // 30;`), because `||` silently overrides a legitimate 0 or empty string.

my %config = (
    debug   => 0,
    timeout => undef,
    host    => 'api.example.com',
);

print exists $config{debug}    ? "yes\n" : "no\n";  # yes
print defined $config{debug}   ? "yes\n" : "no\n";  # yes (0 is defined)
print $config{debug}           ? "yes\n" : "no\n";  # no  (0 is false!)

print exists $config{timeout}  ? "yes\n" : "no\n";  # yes
print defined $config{timeout} ? "yes\n" : "no\n";  # no

print exists $config{missing}  ? "yes\n" : "no\n";  # no
Q10

What is CPAN and how do you install modules?

BasicModules

Answer

CPAN (Comprehensive Perl Archive Network) is Perl's package repository, older and arguably more comprehensive than PyPI or npm. It hosts 200,000+ modules covering everything from web frameworks to bioinformatics. The recommended installer in 2026 is `cpanm` (App::cpanminus) for ad-hoc installs and `carton` for project-level dependency pinning (analogous to npm's package-lock.json).

For system Perl, prefer your distro's package manager (`apt install libfoo-perl`) for core modules; for project Perls use `perlbrew` + `cpanm` + `carton`. Avoid the old interactive `cpan` shell, it works but the UX is from 1995. Practical details interviewers listen for: `cpanm --installdeps .` reads the cpanfile in the current directory, `cpanm -n` skips tests when a known-good distribution has a flaky suite, and `cpm install` is the faster parallel installer many teams moved to.

Never install into the system Perl with sudo on a server, because RHEL and Debian ship tooling that depends on those exact module versions; use `perlbrew` or `plenv` for a project-owned Perl, or `local::lib` plus `PERL5LIB` when you cannot compile one. Mixing distro packages (`apt install libdbi-perl`) with cpanm-installed copies of the same module is the classic 'it worked until we patched the OS' failure, because both land in `@INC` and the winner depends on path order. Verify what is actually loaded with `perl -MDBI -e 'print $DBI::VERSION'` and `perldoc -l DBI`. Carton writes a `cpanfile.snapshot` pinning exact versions for reproducible deploys, and metacpan.org is where you check a distribution's test matrix, last release date and open bugs before you take the dependency.

# Install once globally:
cpanm DateTime
cpanm Mojolicious

# Install from a cpanfile (project deps):
#  cpanfile:
#    requires 'DBI', '>= 1.643';
#    requires 'JSON::XS';
#    requires 'Moo';
carton install                # creates local/ folder
carton exec -- perl app.pl    # run app with local deps

# In your code:
use DateTime;
my $now = DateTime->now(time_zone => 'Asia/Kolkata');
💡 Pro Tip: Prefer pure-Perl modules where possible, XS modules require a C compiler at install time, which is painful on Windows.
Q11

How do you join and split strings in Perl?

BasicStrings

Answer

`split` breaks a string into an array using a regex or string delimiter. `join` does the reverse, joining an array's elements with a separator string. Note the argument order is opposite to what feels natural: `split(DELIMITER, STRING)` but `join(SEPARATOR, LIST)`. `split` with a regex is enormously powerful because the delimiter can be a pattern, `/\s+/` splits on any run of whitespace. A special form, `split(' ', $str)`, has historical magic: it also strips leading whitespace, exactly like `awk`.

Empty trailing fields are stripped by default unless you pass a third LIMIT argument (-1 keeps them all, important for CSV). The parts that catch people out: a positive LIMIT stops splitting after that many fields and leaves the remainder intact in the last one, which is the correct way to parse a record like an `/etc/passwd` line or a log line whose final field is free text (`split /:/, $line, 7`). If the pattern contains capture groups, the captured delimiters are returned in the result list too, which is either a useful trick or an off-by-one surprise. `split //, $str` splits into single characters, and a zero-width pattern such as `split /(?=[A-Z])/, $str` splits before each capital without consuming anything.

A leading empty field is preserved when the string starts with the delimiter, except under the magic `' '` form. And `split` is the wrong tool for real CSV: as soon as a field can contain a quoted comma or an embedded newline you need `Text::CSV_XS`. On the join side, remember it flattens whatever list it gets, so `join ',', %hash` interleaves keys and values in randomised order, and joining an array reference needs an explicit `@$aref` or you stringify the reference itself.

use v5.36;

my $line = 'priya,30,engineer,Bangalore';
my @fields = split /,/, $line;
say "name=$fields[0]";

# Keep trailing empty fields (CSV-safe):
my @csv_fields = split /,/, 'a,b,c,,', -1;   # 5 elements including ''

# Whitespace split with awk-style magic:
my @words = split ' ', '   hello   world  ';   # ('hello', 'world')

# Join back:
my $tsv = join "\t", @fields;                  # priya\t30\tengineer\tBangalore
Q12

What is interpolation in Perl strings?

BasicStrings

Answer

Double-quoted strings interpolate variables and escape sequences, `$name` becomes the value of `$name`, `\n` becomes a newline. Single-quoted strings are literal, `$name` stays as the four characters `$`, `n`, `a`, `m`, `e`. Arrays interpolate too: `"@names"` joins them with `$"` (a space by default).

Inside an interpolated string, to access a hash element use `${hash{key}}` or simply `$hash{key}`, Perl's parser is smart enough to figure it out in most cases. For complex expressions inside strings, use `@{[ ... ]}` (the 'baby cart' operator) to interpolate any expression. Follow-ups worth knowing: the separator used when an array interpolates comes from the list separator variable, a single space by default, and localising it gives you comma-separated output without calling join.

Method calls do not interpolate, so putting `$obj->name` in a double-quoted string produces the stringified reference followed by the literal characters '->name'; use the baby cart form or plain concatenation instead. An unescaped at-sign inside a double-quoted string triggers `Possible unintended interpolation of @example in string`, which is why a literal email address in double quotes needs a backslash before the at-sign. Indented heredocs arrived in Perl 5.26: `<<~'END'` strips the common leading whitespace so the terminator can be indented with the surrounding code, and quoting the terminator decides whether the body interpolates at all. The escape that matters most for correctness is `\Q...\E` (quotemeta), the safe way to drop a user-supplied string into a regex, since otherwise a value containing a dot or a bracket becomes an unintended metacharacter or an outright pattern syntax error.

my $name = 'Aarav';
my @colors = ('red', 'green', 'blue');
my %user = (age => 28);

print "Hello, $name\n";              # Hello, Aarav
print 'Hello, $name\n';               # Hello, $name\n  (literal)

print "Colors: @colors\n";            # Colors: red green blue
print "Age: $user{age}\n";            # Age: 28

# Interpolating an expression:
print "Total: @{[ scalar @colors ]}\n";   # Total: 3
print "Upper: ${\ uc $name }\n";          # Upper: AARAV
Q13

What is the difference between `eq` and `==`, and what breaks when you use the wrong one?

BasicOperators

Answer

Perl has two parallel sets of comparison operators because a scalar can behave as a number or as a string, and the operator you pick decides which. Numeric: `==`, `!=`, `<`, `>`, `<=`, `>=`, `<=>`. String: `eq`, `ne`, `lt`, `gt`, `le`, `ge`, `cmp`.

Choosing wrong fails quietly rather than loudly. `'10' == '10.0'` is true because both sides numify to 10, while `'10' eq '10.0'` is false because the strings differ. Worse, `'abc' == 'abd'` is true, since both strings numify to 0, and the only signal is the warning `Argument 'abc' isn't numeric in numeric eq (==)`, which is invisible if warnings are off. The same split runs through sorting: `sort @nums` is a string sort by default, so 10 comes before 9 and 100 before 20, and you need `sort { $a <=> $b } @nums` for numeric order. `<=>` and `cmp` return -1, 0 or 1 and are the building blocks of every comparator you will write.

Two extras interviewers like: `==` on two references compares addresses, which is a valid identity test although `Scalar::Util::refaddr` states the intent explicitly, and comparing version strings numerically is always wrong because 1.10 is smaller than 1.9 as a number, so use `version->parse`. Remember also that 0, '0', the empty string and undef are false, while '0.0' and '00' are true strings.

use v5.36;

say '10' == '10.0' ? 'equal' : 'differ';   # equal  (numeric compare)
say '10' eq '10.0' ? 'equal' : 'differ';   # differ (string compare)

# Silent trap: both sides numify to 0
say 'abc' == 'abd' ? 'equal' : 'differ';   # equal, plus an isn't-numeric warning

my @nums = (9, 10, 100, 20);
say join ',', sort @nums;                  # 10,100,20,9  (string order!)
say join ',', sort { $a <=> $b } @nums;    # 9,10,20,100

# Multi-key comparator: score descending, then name ascending
my @rows = ({ name => 'priya', score => 90 }, { name => 'aarav', score => 90 });
my @ranked = sort { $b->{score} <=> $a->{score} || $a->{name} cmp $b->{name} } @rows;
say $ranked[0]{name};                      # aarav

# Versions are not numbers
say '1.10' > '1.9' ? 'newer' : 'older';    # older, which is the wrong answer

Key Points

  • == < > <=> are numeric, eq lt gt cmp are string
  • 'abc' == 'abd' is true because both numify to 0
  • sort defaults to string order, pass { $a <=> $b } for numbers
Q14

How do you add, remove and replace elements in a Perl array?

BasicArrays

Answer

`push` and `pop` work at the end of the array, `shift` and `unshift` at the front, and `splice` handles everything in between. Perl arrays keep spare room at the front as well as the end, so `shift` is cheap rather than an O(n) memmove, which is why `my $job = shift @queue;` is the idiomatic work-queue pop. All four modify the array in place and return whatever they removed. `splice(@a, $offset, $length)` removes and returns that run of elements, `splice(@a, $offset, 0, @new)` inserts without removing anything, and `splice(@a, $offset, $length, @new)` replaces a run with a different number of elements, which is the only clean way to do an in-place edit in the middle.

A negative offset counts back from the end. Related basics an interviewer expects you to have straight: `scalar @a` is the element count, `$#a` is the last index and therefore one less, `@a = ()` empties the array and releases the memory, and `$#a = -1` empties it while keeping the allocated buffer, which is a genuine win when you refill the same array in a hot loop. Two things to avoid: `delete $a[3]` leaves a hole and is deprecated on arrays, so use `splice`, and modifying an array while a `for` loop is iterating over it has undefined behaviour, so collect indices and splice afterwards. Assigning past the end silently grows the array with undefs instead of erroring.

use v5.36;
my @q = ('a', 'b', 'c');

push    @q, 'd';         # a b c d    (append)
unshift @q, 'z';         # z a b c d  (prepend)
my $first = shift @q;    # 'z'  -> @q = a b c d
my $last  = pop   @q;    # 'd'  -> @q = a b c

# splice: remove 2 elements starting at index 1
my @gone = splice(@q, 1, 2);        # @gone = (b, c),  @q = (a)

# splice: insert without removing
@q = ('a', 'd');
splice(@q, 1, 0, 'b', 'c');         # @q = a b c d

# splice: replace 1 element with 3
splice(@q, 0, 1, 'x', 'y', 'z');    # @q = x y z b c d

say scalar @q;    # 6  element count
say $#q;          # 5  last index

@q = ();          # empty and free
$#q = -1;         # empty but keep the allocated buffer

Key Points

  • push/pop at the end, shift/unshift at the front, splice in the middle
  • $#a is the last index, scalar @a is the count
  • delete on an array element is deprecated, use splice
Q15

Which special variables should you know for Perl scripting? (`@ARGV`, `%ENV`, `$0`, `$!`, `$@`, `$?`, `$/`, `$|`)

BasicSpecial Variables

Answer

`@ARGV` holds the command-line arguments and, unlike C's argv, does not include the program name, which lives in `$0`. Assigning to `$0` changes what `ps` shows, which is how worker processes label themselves in a prefork pool. Anything beyond two flags belongs in `Getopt::Long`. `%ENV` is the environment, it is writable, and changes propagate to processes you start afterwards.

The error variables are three separate things and confusing them is a classic mistake: `$!` is the errno from the last failed system call and is only meaningful immediately after that failure, `$@` is the error from the last `eval`, and `$?` is the exit status of the last `system` or backticks, packed so the real exit code is `$? >> 8` and the killing signal is `$? & 127`. Testing `if ($?)` alone tells you something failed but not what. `$/` is the input record separator, set to undef for slurp mode and to the empty string for paragraph mode. `$\` is the output record separator, `$,` the output field separator, and `$.` the current input line number. `$|` turns on autoflush for the currently selected handle, and forgetting it is why output interleaves in the wrong order when you pipe to a log file or mix `print` with a `system` call. All of these are package globals shared with every module you load, so change them with `local` inside a block rather than globally.

use v5.36;

say "running $0 with " . scalar(@ARGV) . ' args';
my $file = shift @ARGV or die "usage: $0 <logfile>\n";

open(my $fh, '<', $file) or die "open $file failed: $!\n";   # $! = errno

{
    local $/;                       # slurp mode, restored at block exit
    my $all = <$fh>;
    say length($all), ' characters';
}
close $fh;

my $count = `grep -c ERROR $file`;
if ($? != 0) {
    say 'exit code: ', $? >> 8, '  signal: ', $? & 127;
}

$ENV{TZ} = 'Asia/Kolkata';          # inherited by child processes
STDOUT->autoflush(1);               # same idea as $| = 1 on the selected handle

eval { die "boom\n" };
say "caught: $@" if $@;             # eval error, unrelated to $!
💡 Pro Tip: `$!` is only valid immediately after the failing call, so build the die message in the same statement, not three lines later.
Q16

What are references in Perl and how do you use them?

IntermediateReferences

Answer

Perl arrays and hashes 'flatten' into a single list when passed to functions, which is great for one-liners and terrible for building data structures. References solve this. A reference is a scalar that points to another variable, analogous to a pointer in C or an object reference in Java.

Create with backslash: `\@array`, `\%hash`, `\&sub`. Anonymous structures use brackets: `[]` for an array ref, `{}` for a hash ref, `sub { ... }` for a code ref. Dereference with sigil + arrow: `$$ref` for a scalar, `@{$ref}` for an array, `%{$ref}` for a hash.

The arrow form `$ref->[0]` and `$ref->{key}` is what you'll use 95% of the time. Without references you can't build nested structures, pass arrays cleanly to subs, or do OOP. Three things a senior asks about. `ref($x)` returns 'ARRAY', 'HASH', 'CODE', 'SCALAR', 'GLOB' or 'Regexp' for a plain reference but the class name for a blessed one, so use `Scalar::Util::blessed` to ask whether something is an object and `reftype` to ask what it is underneath.

Postfix dereferencing, stable since Perl 5.24, reads far better for chained access: `$data->{rows}->@*` instead of `@{ $data->{rows} }`, and `$h->%{qw(a b)}` for a key-value slice. Perl reclaims memory by reference counting rather than tracing garbage collection, so a cycle (a parent holding children that hold the parent) is never freed, and you break it with `Scalar::Util::weaken` on the back-link. Copies are shallow: `my %copy = %$href;` duplicates only the top level and shares every nested reference, so reach for `Storable::dclone` when you genuinely need a deep copy. Finally, `$$ref[0]`, `${$ref}[0]` and `$ref->[0]` are the same operation, and the arrow form is the one code review expects.

use v5.36;

# Anonymous structures (most common):
my $user = {
    name   => 'Priya',
    skills => ['Perl', 'SQL', 'Bash'],
    addr   => { city => 'Bangalore', pincode => 560001 },
};

say $user->{name};                    # Priya
say $user->{skills}[0];               # Perl     (arrow optional between subscripts)
say $user->{addr}{city};              # Bangalore

# Build dynamically:
push @{ $user->{skills} }, 'Python';

# Reference to existing variable:
my @numbers = (1, 2, 3);
my $ref = \@numbers;
push @$ref, 4;                        # @numbers is now (1,2,3,4)

Key Points

  • References are required for nested data structures
  • Anonymous: [] for array ref, {} for hash ref
  • Arrow ->{} ->[] to access through references
  • Sigil between subscripts is optional: $h->{a}{b} works
Q17

How does Perl handle OOP? Compare classic Perl OOP, Moose, and Moo.

IntermediateOOP

Answer

Perl had OOP retrofitted onto it in version 5 (1994) and the original mechanism is shockingly minimal: a 'class' is a package, an 'object' is a blessed reference, and 'methods' are subroutines that take the object as the first argument. There's no built-in syntax for attributes, type checks, or inheritance helpers, you write them yourself. **Moose** (2006) fixed this with a real OOP framework: declarative attributes, type constraints, roles (Perl's mixin/trait system), method modifiers (`before`/`after`/`around`). Moose is powerful but heavy, startup time and memory cost are significant. **Moo** is a minimalist Moose subset: same syntax, no type system depth, much lighter and faster.

Rule of thumb in 2026: use Moo for CLI tools and modules, Moose for large applications, and the new built-in `class` feature (Perl 5.38+, still experimental) if you don't need ecosystem compatibility. Mechanics you should be able to recite: inheritance lives in the package's `@ISA`, normally set with `use parent -norequire, 'Base';`, method resolution is depth-first by default, and `use mro 'c3';` switches to C3 for diamond hierarchies. `$self->SUPER::method(@args)` dispatches from the parents of the package the call is written in, not the object's actual class, which is why it behaves oddly inside roles and runtime-generated methods. `->can('name')` returns a code ref or undef, while `->isa` and `->DOES` answer type questions. `AUTOLOAD` catches unknown method calls and is painful to debug, and `DESTROY` runs at unpredictable points during global destruction, so never rely on it to flush a file or commit a transaction. In Moose and Moo, `BUILDARGS` normalises constructor arguments, `BUILD` does post-construction validation, `lazy => 1` with a `builder` defers expensive work, and roles composed with `with` are the preferred answer to multiple inheritance. Moo transparently upgrades itself to Moose if Moose is loaded, and `Type::Tiny` gives Moo real type constraints without the Moose weight.

# Classic Perl OOP:
package Point;
sub new {
    my ($class, %args) = @_;
    return bless { x => $args{x}, y => $args{y} }, $class;
}
sub x { $_[0]->{x} }
sub y { $_[0]->{y} }

# With Moo (recommended for most cases):
package Point;
use Moo;

has x => (is => 'ro', required => 1);
has y => (is => 'ro', required => 1);

sub distance_to {
    my ($self, $other) = @_;
    return sqrt(($self->x - $other->x)**2 + ($self->y - $other->y)**2);
}

# Usage:
my $p = Point->new(x => 3, y => 4);
say $p->x;   # 3
Q18

What's the difference between `=~` and `!~`?

IntermediateRegex

Answer

`=~` binds a regex match/substitution to a scalar (something other than `$_`). It evaluates to whatever the regex returns, true on match, false on no match, count on `g` substitution. `!~` is the negation, true when the regex doesn't match. Both are essential because without them, regex operations default to operating on `$_`, which is rarely what you want in serious code.

The classic interview trap: `s///` with `!~` always evaluates to false, because `!~` only makes sense for matches. The opposite trap: `$line =~ /pattern/` looks like assignment to a junior, it's not, the regex on the right is the operator, the variable on the left is the target. Two more points earn credit.

The binding operator has high precedence, tighter than `!`, `&&` and assignment, so `my $ok = $str =~ /x/;` does what you expect while `!$str =~ /x/` negates `$str` first and matches against the result, a bug that reads as correct. You can also bind to anything assignable, not just a plain scalar: `$h->{name} =~ s/^\s+//;` works fine, and the copy-then-modify idiom `(my $clean = $raw) =~ s/\s+/ /g;` is how people did non-destructive substitution before the `/r` modifier arrived in 5.14. When the left side is not modifiable you get `Modification of a read-only value attempted`, which is exactly what happens if you run `s///` across a literal list, over `$_` inside a `map` on constants, or over `@_` elements aliased to constants. And `!~` with `tr///` compares the translation count against zero rather than testing a pattern, so `$dna !~ tr/N//` means 'contains no N', legal but obscure enough that most reviewers will ask for the explicit form.

use v5.36;

my @logs = ('INFO: started', 'ERROR: db down', 'INFO: ok');

# Keep only errors:
my @errors = grep { $_ =~ /^ERROR/ } @logs;

# Keep only non-errors:
my @others = grep { $_ !~ /^ERROR/ } @logs;

# Substitution must use =~ (writes back to the scalar):
for my $line (@logs) {
    $line =~ s/^INFO/info/;
}

# Common bug, `$line !~ s///` is almost certainly wrong:
# if ($x !~ s/foo/bar/) { ... }      # ← suspicious, code review fail
Q19

What are regex modifiers and when do you use them?

IntermediateRegex

Answer

Modifiers go after the closing delimiter and change how the regex engine behaves. `i` ignores case. `g` makes substitution global or, in list context, returns all matches. `s` lets `.` match newlines (single-line mode, confusingly named). `m` makes `^` and `$` match line boundaries within a multi-line string. `x` enables free-spacing mode, you can put whitespace and `#` comments inside the regex, making complex patterns readable. `e` (substitution only) treats the replacement as Perl code. `r` (Perl 5.14+) returns the modified string instead of modifying in place, useful in functional pipelines. The combination you'll use most: `xs` for any pattern longer than two lines. The modifiers people forget are the character-set ones, and they matter with Indian production data. `/a` restricts `\d`, `\w` and `\s` to ASCII, so a validator written as `/^\d+$/a` will not silently accept Devanagari or Bengali digits pasted into a form once the input has been decoded to characters. `/u` forces Unicode semantics regardless of locale, `/l` follows the current locale, and `/d` is the old default-dependent behaviour you almost never want in new code. `/n` (5.22) makes plain parentheses non-capturing, so you can group without renumbering every `$1`. `/xx` (5.26) extends free-spacing into character classes. `/o` (compile once) is a relic superseded by `qr//`. `/ee` evaluates the replacement twice and is a code-injection hole if any part of it comes from user input. `/gc` preserves `pos()` on a failed match, which is what a `\G`-anchored lexer needs so it can try the next token pattern. Modifier order does not matter, and `(?i:...)` sets a flag for part of a pattern only, which is safer than making an entire pattern case-insensitive.

use v5.36;

my $email = 'PRIYA@example.com';
say $email =~ /priya/i ? 'match' : 'no';   # match (case-insensitive)

# Free-spacing for readability:
my $phone_re = qr{
    ^\+?91[-\s]?     # optional +91 country code
    (\d{5})          # 5 digits
    [-\s]?
    (\d{5})$         # 5 digits
}x;

# /e, evaluate replacement as code:
my $text = 'price=100 price=250';
$text =~ s/price=(\d+)/'price=' . ($1 * 1.18)/ge;   # add 18% GST

# /r, non-destructive substitution:
my $shouty = 'hello';
my $quiet  = $shouty =~ s/h/H/r;   # $shouty unchanged, $quiet = 'Hello'
Q20

How do you connect Perl to a database with DBI?

IntermediateDatabase

Answer

DBI is Perl's universal database interface, same API regardless of MySQL, Postgres, Oracle, or SQLite, with a driver (DBD::*) per backend. It's been the standard since 1995 and is one of the reasons Perl is still load-bearing in Indian banking, telecom, and BPO backends. Connect with `DBI->connect()`, prepare a statement, bind parameters (NEVER interpolate, SQL injection), execute, fetch.

For read-heavy code use `selectall_arrayref`/`selectrow_hashref` for terseness. Always set `RaiseError => 1` to make DB errors throw rather than silently fail. For modern apps consider DBIx::Class (full ORM) or Mojo::Pg / Mojo::mysql for async-friendly access.

Production details separate a maintainer from a beginner here. Set `AutoCommit => 0` and wrap writes in `$dbh->begin_work; ... $dbh->commit;` with a `rollback` in the error path, because with `AutoCommit => 1` a half-finished batch leaves the tables inconsistent. Placeholders cannot stand in for table names, column names or an IN list, so build the list as `join(',', ('?') x @ids)` and pass the ids to `execute`.

For bulk reads, `fetchall_arrayref({})` returns an array of hash refs in a single call and `selectall_hashref` keys the result by a column. Never share a `$dbh` across a `fork`: the child inherits the same socket and both processes then corrupt the wire protocol, so connect after forking or set `AutoInactiveDestroy => 1` so a child's destructor cannot close the parent's connection. `mysql_auto_reconnect` looks helpful and quietly breaks transactions and temporary tables by reconnecting mid unit of work. For MariaDB, and for correct utf8mb4 handling generally, DBD::MariaDB is now the recommended driver over DBD::mysql. Round it off with `$dbh->last_insert_id` after inserts and `$sth->err`, `$sth->errstr` and `$sth->state` for driver-level diagnostics.

use v5.36;
use DBI;

my $dbh = DBI->connect(
    'DBI:mysql:database=accounts;host=db.internal',
    $ENV{DB_USER}, $ENV{DB_PASS},
    { RaiseError => 1, AutoCommit => 1, mysql_enable_utf8 => 1 }
) or die $DBI::errstr;

# Always use placeholders, never interpolate:
my $sth = $dbh->prepare(
    'SELECT id, name, balance FROM customers WHERE city = ? AND status = ?'
);
$sth->execute('Bangalore', 'active');

while (my $row = $sth->fetchrow_hashref) {
    say "$row->{id}: $row->{name} (₹$row->{balance})";
}

# One-liner for known-small result:
my $count = $dbh->selectrow_array(
    'SELECT COUNT(*) FROM transactions WHERE created_at >= ?',
    undef,
    '2026-05-01'
);
💡 Pro Tip: RaiseError => 1 + a transaction (begin_work / commit / rollback) is the safe pattern for any multi-statement write.
Q21

What's the difference between `map`, `grep`, and `for` in Perl?

IntermediateList Operations

Answer

All three iterate over a list, but with different intent. `map BLOCK LIST` evaluates the block for each element (with the element in `$_`) and returns a list of results, use it to transform. `grep BLOCK LIST` evaluates the block and returns the elements for which the block was true, use it to filter. `for` (or `foreach`, identical) is the imperative loop, use it for side effects (printing, modifying external state). Chain them like Unix pipes. The common bug: using `map` for side effects creates a return list nothing consumes, wasting memory; using `for` to build a list requires manual push, when `map` is clearer.

The gotchas an interviewer probes: `map` and `grep` alias `$_` to the original element, so modifying `$_` inside the block mutates the source list, occasionally deliberate and usually a bug. The parser cannot always tell whether `{` opens a block or an anonymous hash, so `map { { name => $_ } } @list` needs a disambiguating `+{` or a leading semicolon, and without it you get a syntax error near the fat comma that reads like nonsense. `grep` in scalar context returns the count, which is the idiomatic way to express 'how many rows match'. If you only need the first match or a yes/no answer, `List::Util`'s `first`, `any` and `all` short-circuit whereas `grep` always scans the entire list, and that difference is measurable on a million-row array.

Building a lookup with `my %by_id = map { $_->{id} => $_ } @rows;` is the standard fix for an accidental O(n squared) nested search, which is the most common real performance bug in Perl reporting scripts. Note also that `last`, `next` and `redo` are not allowed inside `map` or `grep` blocks: if you need them, you wanted a `for` loop.

use v5.36;

my @users = (
    { name => 'Priya', age => 28, active => 1 },
    { name => 'Bob',   age => 45, active => 0 },
    { name => 'Aarav', age => 22, active => 1 },
);

# Transform: extract names
my @names = map { $_->{name} } @users;

# Filter: only active users
my @active = grep { $_->{active} } @users;

# Both: names of active users under 30
my @young = map  { $_->{name} }
            grep { $_->{active} && $_->{age} < 30 } @users;

# Imperative, only when you really need side effects:
for my $u (@users) {
    say "$u->{name}: " . ($u->{active} ? 'active' : 'inactive');
}
Q22

What is a closure in Perl?

IntermediateSubroutines

Answer

A closure is an anonymous subroutine that captures variables from its enclosing lexical scope. When you create an anonymous sub inside a function, any `my` variable in scope at the time of creation lives on as long as the closure does, even after the outer function returns. This is the foundation of all functional patterns in Perl: callbacks, partial application, iterators, memoization.

The most subtle gotcha: `my` variables are captured by reference, not value, so multiple closures inside the same loop iteration can share state. To make per-iteration captures, declare the `my` variable inside the loop body. The interview version of this is the loop question. `for my $i (1 .. 3) { push @subs, sub { $i } }` behaves correctly because `my $i` is a fresh variable on each iteration, so the three closures capture three different scalars.

Rewrite it as `my $i; for $i (1 .. 3)` or as a C-style `for (my $i = 0; $i < 3; $i++)` and every closure shares one variable, so all of them return the final value. The related diagnostic is `Variable $x will not stay shared`, which Perl emits when you nest a named sub inside another named sub: the inner sub is compiled once and keeps the first instance of the outer lexical forever. Closures also keep their captured variables alive, so a callback that closes over `$self` and is stored inside `$self` creates a reference cycle that reference counting cannot collect, the standard leak in event-loop code; the fix is to copy the invocant and call `Scalar::Util::weaken` on the copy before building the callback. For a single counter with no factory involved, a `state` variable is lighter than a closure.

use v5.36;

sub make_counter ($start = 0) {
    my $count = $start;          # captured by the closure below
    return sub { return ++$count };
}

my $c1 = make_counter();
my $c2 = make_counter(100);
say $c1->();   # 1
say $c1->();   # 2
say $c2->();   # 101
# $c1 and $c2 each have their own private $count

# Memoization:
sub memoize ($fn) {
    my %cache;
    return sub ($key) {
        $cache{$key} //= $fn->($key);   # //= = assign if undef
        return $cache{$key};
    };
}

my $expensive = memoize(sub ($n) { sleep 1; $n * 2 });
say $expensive->(5);   # slow first time
say $expensive->(5);   # instant
Q23

How do you write a Perl one-liner to process a log file?

IntermediateOne-liners

Answer

One-liners are where Perl genuinely still beats Python: shorter, faster to type, no boilerplate. The key flags: `-e` runs the code on the command line. `-n` wraps your code in `while (<>) { ... }` (read each line into `$_`). `-p` is `-n` plus auto-print of `$_` at the end of each iteration. `-l` strips newlines on input and adds them on output. `-a` auto-splits each line into `@F` (like awk). `-F` sets the split delimiter. Combine them: `perl -lane '...'` is a Swiss army knife for log processing.

The classic interview question: 'how would you sum the third column of a CSV?', `perl -F, -lane '$sum += $F[2]; END { print $sum }'`. The flags that round out the set: `-0777` slurps an entire file into `$_` so a substitution can cross line boundaries, `-00` switches to paragraph mode, `-l` handles both the input chomp and the output record separator, `-M` loads a module, `-E` behaves like `-e` but enables `say` and the current feature bundle, and `-c` only syntax-checks. With several input files, `$ARGV` holds the current filename and `eof` without parentheses distinguishes the end of one file from the end of all of them.

Two production warnings. `perl -i -e` with no backup suffix rewrites files with no undo, so use `-i.bak` on anything you care about and dry-run the same expression with `-p` first. In-place editing also writes a new inode, which breaks any process still holding the old file open and silently defeats a hard link, a genuine surprise when you edit a log that a daemon is writing. Shell quoting is the other trap, because a one-liner wrapped in single quotes cannot contain an apostrophe, so either switch quoting styles or spell the character as `\x27`. `BEGIN` and `END` blocks work inside `-e`, which is how you accumulate then print totals.

# Sum the 3rd column of access.csv:
perl -F, -lane '$s += $F[2]; END { print $s }' access.csv

# Print only ERROR lines from huge log:
perl -ne 'print if /ERROR/' /var/log/app.log

# In-place edit, replace 'foo' with 'bar' in all .conf files:
perl -pi.bak -e 's/foo/bar/g' *.conf
#       ^^ -pi.bak keeps a .bak backup of each file

# Top 10 most frequent IPs in nginx log:
perl -lane '$c{$F[0]}++; END { for (sort { $c{$b} <=> $c{$a} } keys %c) { print "$c{$_}\t$_"; last if ++$n >= 10 } }' access.log

# Convert tabs to commas in a TSV:
perl -pe 's/\t/,/g' input.tsv > input.csv

Key Points

  • -n / -p loop, -l handles newlines, -a auto-splits
  • -i for in-place edits with backup
  • Perl one-liners often beat awk for anything regex-heavy
Q24

What is the autovivification problem in Perl?

IntermediateReferences

Answer

Autovivification is the feature where accessing a non-existent element of a hash or array reference automatically creates the intermediate structure. `$h->{a}{b}{c} = 1` creates `$h->{a}` as a hash ref, `$h->{a}{b}` as another hash ref, and finally sets `$h->{a}{b}{c} = 1`. This is enormously convenient for building data structures, but it's also a frequent bug source: reading a non-existent path also autovivifies. `if ($h->{users}{$id}{name} eq 'Priya') { ... }` will create `$h->{users}` and `$h->{users}{$id}` as empty hashes even if the user never existed. Defense: check with `exists` before deep access, or use the `autovivification` pragma to disable it for a block.

Especially dangerous in long-running services where hashes grow forever. The rule to memorise is that any dereference in lvalue position autovivifies, and Perl treats a nested subscript chain as lvalue context all the way down to the final element. That is why `exists $h->{a}{b}` still creates `$h->{a}`, and why passing `$h->{a}{b}` as a subroutine argument creates it too: `@_` aliases its arguments and therefore needs a real container to alias.

In a long-running Bugzilla-style or Request Tracker-style process, a per-request lookup on an unknown key grows the hash forever, and the symptom is resident memory that climbs steadily while the request rate stays flat. Diagnose it by logging `scalar keys %hash` on a timer rather than by rereading the code. Fix it with explicit `exists` chains, a lexical copy, or `no autovivification qw(fetch exists delete);` scoped to the file. The good half deserves defending as well: `push @{ $index{$key} }, $row;` builds a grouped index with no initialisation ceremony, which is why nobody actually wants the feature removed.

use v5.36;

my %db;

# This 'just works' (autovivification creates the path):
$db{users}{1042}{name} = 'Priya';
push @{ $db{users}{1042}{tags} }, 'engineer';

# Surprise: a read also creates the structure!
if ($db{users}{9999}{name}) { ... }
# Now %db has an entry for user 9999 with an empty hash, even though we only read.

# Defensive:
if (exists $db{users}{9999} && $db{users}{9999}{name}) { ... }

# Or disable for a block:
use autovivification;
no autovivification qw(fetch);
my $name = $db{users}{8888}{name};   # now returns undef without creating entries
Q25

How does Perl handle exceptions?

IntermediateError Handling

Answer

Perl's built-in exception mechanism is `die` to throw and `eval { ... }` to catch. After `eval`, `$@` holds the error message (empty if no error). It's primitive, exceptions are strings by default, no built-in stack trace, no typed catch.

Modern Perl wraps this with `Try::Tiny` (lightweight, fixes several `eval`/`$@` edge cases) or the experimental `try`/`catch` syntax stabilizing in Perl 5.40+. Best practice: throw exception objects (blessed refs) rather than plain strings, so callers can pattern-match on them. The `Throwable` and `Exception::Class` modules give you OOP exceptions.

The classic gotcha: `if ($@) { ... }` can be wrong because something else might have set `$@` between the `eval` and the check, use `Try::Tiny` to avoid this trap. Details that make this a senior question: `die` appends ` at FILE line N.` unless your message already ends with a newline, so end user-facing messages with a newline and leave it off for internal errors where the location helps. `eval BLOCK` is compiled with the rest of the program, whereas `eval STRING` compiles at runtime, which is slow and a code-injection hole if any user input reaches it. Testing `if ($@)` gives a false negative when the exception is the string '0' and a false positive when a destructor running during stack unwinding overwrites `$@`, which is precisely the class of bug Try::Tiny was written to paper over.

Try::Tiny has a trap of its own: `catch` is a subroutine, so `return` inside it returns from the catch block rather than from the enclosing function. The native `try`/`catch` (feature 'try' from 5.34, no longer experimental as of 5.40) does not suffer from that, so prefer it when your minimum Perl allows. Finally, `$SIG{__DIE__}` fires even inside an `eval`, so a global handler that logs and rethrows can turn a perfectly handled error into a crash.

use v5.36;
use Try::Tiny;

sub charge_card ($amount) {
    die { type => 'INVALID', msg => 'amount must be > 0' } if $amount <= 0;
    # ... actually charge ...
    return { txn_id => 'tx_123' };
}

my $result = try {
    charge_card(-50);
} catch {
    my $err = $_;   # Try::Tiny puts the error in $_, not $@
    if (ref $err eq 'HASH' && $err->{type} eq 'INVALID') {
        warn "validation error: $err->{msg}";
        return { ok => 0 };
    }
    die $err;       # rethrow unknown errors
};

# Built-in try/catch (Perl 5.34+, no module needed):
use feature 'try'; no warnings 'experimental::try';

try { charge_card(-50) }
catch ($e) {
    warn "failed: $e";
}
Q26

What is the difference between `wantarray`, scalar, and void context inside a subroutine?

IntermediateContext

Answer

Inside a subroutine, `wantarray` tells you the context the caller used: true for list context, false (but defined) for scalar context, undef for void context (caller ignored the return value). This lets a single subroutine return different things based on how it's called, `localtime` and `caller` are core examples. Most well-designed APIs avoid wantarray-dependent behavior because it surprises maintainers; explicit functions are usually clearer.

But for a few cases (an iterator that returns a list of remaining items in list context and the next item in scalar context, or skipping expensive work in void context for logging functions), `wantarray` is the right tool. What interviewers actually test is the failure mode. `wantarray` reports the caller's context, so a helper that behaves differently depending on how it was called becomes hard to test the moment somebody wraps it, because the wrapper's context is what the helper sees. There is no boolean context to detect either: `if (f())` is indistinguishable from ordinary scalar context.

The related trap is the return statement itself. `return;` yields an empty list in list context and `undef` in scalar context, which is exactly what 'no result' should mean, whereas `return undef;` yields a one-element list, so `if (my @rows = f())` evaluates true even when the call failed. Perl Best Practices argues against the `wantarray ? @list : \@list` idiom for this reason, and modern APIs return a reference consistently and let the caller dereference. The uses that survive code review are narrow: skipping expensive formatting when the result is discarded, and warning in void context that a pure function was called for nothing.

use v5.36;

sub fetch_user ($id) {
    my $row = $db->selectrow_hashref('SELECT * FROM users WHERE id = ?', undef, $id);
    return unless $row;

    if (wantarray) {
        return ($row->{name}, $row->{email}, $row->{age});  # list context
    } elsif (defined wantarray) {
        return $row;                                         # scalar context
    } else {
        # void context, caller ignored the return. skip the heavy work.
        return;
    }
}

my $user = fetch_user(42);                 # scalar, returns hashref
my ($name, $email, $age) = fetch_user(42); # list, returns 3-element list
fetch_user(42);                            # void, skip building output
Q27

What are slices in Perl?

IntermediateData Structures

Answer

A slice extracts multiple elements from an array or hash in one expression. Array slice: `@array[1, 3, 5]` returns elements at those indices. Hash slice: `@hash{qw(a b c)}` returns the values for those keys.

Note the sigil, slices return lists, so they use `@`, even on a hash. There's also a hash slice that returns key-value pairs (Perl 5.20+): `%hash{qw(a b c)}` returns a hash of just those keys. Slices are the idiomatic way to copy a subset of a hash, build a hash from parallel arrays, or assign to multiple keys at once.

They're more efficient than a loop and read cleaner. A few extras come up in follow-ups. Perl 5.20 added key-value slices, so `%h{'a','b'}` gives back keys with values as a hash and `%a[0,1]` gives index-value pairs from an array, which is what you want when you need to know which keys were actually present rather than just their values.

Negative indices work inside slices, so `@arr[-2, -1]` is the last two elements. Slices reach through references with the old form `@{$href}{qw(a b)}` or, since 5.24, with postfix `$href->@{qw(a b)}` and `$href->%{qw(a b)}`. `delete @h{qw(a b)}` deletes several keys at once and returns the deleted values. Two idioms are worth memorising: `@arr[0,1] = @arr[1,0]` swaps in place with no temporary, and `@seen{@list} = ();` builds a set cheaply, where the values are undef but `exists` is true, which is how deduplication was written before `List::Util::uniq`. The warning `Scalar value @arr[0] better written as $arr[0]` means you used slice syntax for a single element: harmless, but it usually signals that you have the wrong context in mind.

use v5.36;

my @colors = ('red', 'green', 'blue', 'yellow', 'pink');
my @rgb    = @colors[0, 1, 2];                      # ('red', 'green', 'blue')

my %user = (
    id     => 42,
    name   => 'Priya',
    email  => 'priya@example.com',
    secret => 'hunter2',
);

# Value slice, public fields only:
my @public = @user{qw(id name email)};               # (42, 'Priya', 'priya@...')

# Hash slice (5.20+), copy subset of hash:
my %public_user = %user{qw(id name email)};          # without 'secret'

# Set multiple keys at once:
@user{qw(role city)} = ('engineer', 'Bangalore');

# Build hash from parallel arrays:
my @keys = ('a', 'b', 'c');
my @vals = (1, 2, 3);
my %h;
@h{@keys} = @vals;                                   # %h = (a => 1, b => 2, c => 3)
Q28

How do you debug Perl code in production? Data::Dumper, Carp, and the debugger.

IntermediateDebugging

Answer

Three tools you'll reach for daily. **`Data::Dumper`** is print-debugging on steroids, give it any reference, get back a stringified dump that's also valid Perl. For modern code, `Data::Printer` (`p $ref`) gives nicer output with colors and depth limits. **`Carp`** replaces `warn`/`die` with `carp`/`croak` (report from the caller's perspective, so you see where the bug came from rather than where it was caught) and `cluck`/`confess` (with full stack traces). Always use Carp in modules, plain `die` from a library tells the user where in your code it failed, not where in their code they used it wrong. **The interactive debugger** (`perl -d script.pl`) is genuinely good, set breakpoints with `b`, step with `s`/`n`, dump variables with `x`.

For production tracing without a debugger attached, use `Devel::Trace` to print every line executed, or stick `printf STDERR` calls behind a `$DEBUG` flag. Settings that make Data::Dumper usable on real objects: `$Data::Dumper::Maxdepth = 2` so dumping a record does not print an entire DBI handle and its connection state, `$Data::Dumper::Terse = 1` to drop the `$VAR1 =` prefix, and `$Data::Dumper::Useqq = 1` to expose trailing whitespace and control characters, which is how you finally see that a parse failure was a stray carriage return from a Windows-generated file. Inside the debugger, `T` prints the stack, `b Some::Module::func` breaks on a subroutine, `w $var` watches a variable, and assigning `$DB::single = 1` in the source drops you at the prompt at that exact point on the next run. For a process already running in production the pattern that actually works is a signal handler installed at startup, so `kill -USR1 <pid>` makes the daemon dump its current state to the log without a restart, and structured logging through Log::Log4perl or Log::Any beats scattered `print STDERR` because you can raise the level for one package at runtime.

use v5.36;
use Data::Dumper;
use Carp;

my $config = {
    db    => { host => 'localhost', port => 3306 },
    users => ['alice', 'bob'],
};

# Quick dump:
local $Data::Dumper::Sortkeys = 1;     # stable output for diff-ing
local $Data::Dumper::Indent   = 1;
print Dumper($config);

# Modules should use Carp, not die:
sub connect_db ($args) {
    croak 'host is required' unless $args->{host};        # die-like, blames caller
    carp  'using default port 3306' unless $args->{port}; # warn-like, blames caller
    confess 'unexpected internal error' if $args->{port} < 0;  # die + stack trace
}

# Run with the interactive debugger:
#   perl -d my_script.pl
# Inside the debugger: 's' to step, 'n' to next, 'p $var' to print, 'x \%hash' to dump.
💡 Pro Tip: Set `$Data::Dumper::Sortkeys = 1` so hash dumps are deterministic, essential for diff-ing two snapshots.
Q29

How do you sort complex data in Perl, and what is the Schwartzian Transform?

IntermediateSorting

Answer

`sort` with no block does a string comparison, so numbers come back in dictionary order. Pass a block that returns negative, zero or positive, using `<=>` for numbers and `cmp` for strings. The two values arrive in the package globals `$a` and `$b` rather than as arguments, which is why a named comparator defined in another package silently misbehaves: it reads its own package's `$a` and `$b`.

Chain keys with `||`, because the comparison operators return 0 on a tie, so `$b->{score} <=> $a->{score} || $a->{name} cmp $b->{name}` sorts by score descending then name ascending. Perl has used a stable mergesort since 5.8 and you can state the requirement with `use sort 'stable';`. The Schwartzian Transform matters as soon as the sort key is expensive to compute. `sort` performs O(n log n) comparisons and evaluates the block for each one, so a key computed inside the block is recomputed roughly two n log n times: sorting 10,000 files by `-s $_` would stat the filesystem hundreds of thousands of times.

The transform is map, sort, map: build `[$key, $item]` pairs once, sort on the precomputed key, then map back to the items. Two closing points: `List::Util`'s `max`, `min` and `reduce` are single-pass and beat sorting when you only need one element, and `cmp` compares Unicode code points, so ordering Devanagari or accented text the way a human expects needs `Unicode::Collate`.

use v5.36;
use List::Util qw(max);

my @files = glob '*.log';

# Naive: -s runs a stat inside every single comparison
my @slow = sort { -s $a <=> -s $b } @files;

# Schwartzian Transform: exactly one stat per file
my @fast = map  { $_->[1] }
           sort { $a->[0] <=> $b->[0] }
           map  { [ -s $_, $_ ] } @files;

# Multi-key: score descending, then name ascending
my @rows = (
    { name => 'aarav', score => 90 },
    { name => 'priya', score => 90 },
    { name => 'ravi',  score => 75 },
);
my @ranked = sort { $b->{score} <=> $a->{score}
                 || $a->{name}  cmp $b->{name} } @rows;

# Sort hash keys by their value
my %hits = (home => 40, jobs => 120, blog => 7);
my @top  = sort { $hits{$b} <=> $hits{$a} } keys %hits;
say $top[0];        # jobs

# Only need the largest? Do not sort at all.
say max(values %hits);   # 120
💡 Pro Tip: If the comparator calls a function, you almost certainly want a Schwartzian Transform instead.
Q30

How do you produce and consume JSON in Perl, and what goes wrong with numbers and booleans?

IntermediateSerialization

Answer

Use `JSON::MaybeXS`, which loads `Cpanel::JSON::XS` when it is available and falls back to `JSON::PP` (core since Perl 5.14), so the same code runs everywhere and runs fast where the XS module is installed. `encode_json` and `decode_json` deal in UTF-8 encoded bytes, while the object interface (`JSON::MaybeXS->new->utf8->canonical->pretty`) gives you the knobs. Turn on `canonical` whenever output is hashed, signed, diffed or committed to git, because hash key order is randomised per process and otherwise identical data produces different bytes on every run. Two gotchas cause most real bugs.

Booleans: Perl has no boolean type, so JSON true and false decode into `JSON::PP::Boolean` objects that behave as 1 and empty string, and encoding a plain 1 gives you the number 1, not true. From Perl 5.36, `builtin::true`, `builtin::false` and `builtin::is_bool` finally provide a native way to round-trip them. Numbers: a scalar remembers whether it was last used as a string, so an id you interpolated into a log line before encoding is serialised as a quoted string instead of a number, which breaks strict consumers written in Go or Java.

Force it with `0 + $id` for a number, or concatenate an empty string to force a string. For untrusted input set `max_depth` and `max_size` so a deeply nested payload cannot exhaust the stack, use `convert_blessed` plus a `TO_JSON` method to serialise objects, and `incr_parse` to stream documents too large to hold in memory.

use v5.36;
use JSON::MaybeXS;

my $json = JSON::MaybeXS->new(utf8 => 1, canonical => 1);

my $id  = 42;
my $log = "processing id=$id";          # $id now carries a string slot too

say $json->encode({ id => $id });       # {"id":"42"}  <- quoted, a bug
say $json->encode({ id => 0 + $id });   # {"id":42}    <- forced numeric

# Booleans decode to objects, not to 1 and 0
my $data = $json->decode('{"active":true,"count":3}');
say ref $data->{active};                # JSON::PP::Boolean
say $data->{active} ? 'yes' : 'no';     # yes

# Native booleans, Perl 5.36+
use builtin qw(true false);
no warnings 'experimental::builtin';
say $json->encode({ active => true });  # {"active":true}

# Untrusted input: cap the damage
my $safe = JSON::MaybeXS->new(utf8 => 1)->max_depth(32)->max_size(1_000_000);

Key Points

  • JSON::MaybeXS picks the XS backend when present, JSON::PP otherwise
  • canonical(1) for reproducible bytes, hash order is randomised
  • 0 + $x forces a JSON number, a stringified scalar serialises as a quoted string
Q31

How do you write and run tests for a Perl codebase?

IntermediateTesting

Answer

Perl's testing story predates most other languages' and is built on TAP (Test Anything Protocol): a test file prints `ok 1` and `not ok 2` lines, and a harness aggregates them. `prove -lr t/` runs everything under `t/` with `lib/` added to `@INC`, `-v` shows each assertion, and `-j4` runs files in parallel, which immediately exposes tests that quietly share a fixture file or a database table. Classic suites use `Test::More` (`ok`, `is`, `isnt`, `like`, `is_deeply`, `subtest`, and `done_testing` rather than a hard-coded plan). New code should use `Test2::V0`, which bundles the modern equivalents plus deep-comparison helpers (`hash`, `array`, `field`, `etc`) so you can assert on the part of a structure you care about without freezing every field into the test.

For isolation, `Test::MockModule` replaces one subroutine at a time, an in-memory SQLite handle or `DBD::Mock` stands in for the database, and `Test::Mojo` or `Plack::Test` exercise a web app without binding a port. Measure coverage with Devel::Cover through `cover -test`, which reports statement, branch and condition coverage separately. Keep author-only checks such as Test::Pod, spelling and Perl::Critic in `xt/` so users installing from CPAN never fail on your house style. The failure mode worth naming in an interview: tests that assume hash ordering or a local timezone pass on your laptop and fail in CI, because key order is randomised per process and CI runs in UTC.

# t/10-parser.t
use Test2::V0;
use MyApp::Parser;

my $out = MyApp::Parser->parse('user=priya code=401');

is($out->{user}, 'priya', 'user extracted');
is($out->{code}, 401,     'code is numeric');

# Assert on part of the structure, ignore everything else
is($out, hash { field user => 'priya'; etc; }, 'shape looks right');

like(dies { MyApp::Parser->parse(undef) }, qr/input required/, 'dies on undef');

subtest 'edge cases' => sub {
    is(MyApp::Parser->parse(''),          undef, 'empty line ignored');
    is(MyApp::Parser->parse('# comment'), undef, 'comment ignored');
};

done_testing;

# prove -lr t/              run everything
# prove -lv t/10-parser.t   one file, verbose
# prove -lr -j4 t/          parallel, exposes shared-state bugs
# cover -test               coverage via Devel::Cover
💡 Pro Tip: Run `prove -j4` at least once before you trust a suite, serial-only passes hide shared fixtures.
Q32

How do you write a CPAN-quality Perl module in 2026?

AdvancedModules

Answer

A modern Perl module ships with: (1) A clean namespace, `My::Org::Foo` for organisation modules, with no top-level grab. (2) `Module::Build::Tiny` or `ExtUtils::MakeMaker` for installation, declared in `Makefile.PL` or `Build.PL`. (3) A `cpanfile` listing runtime + test dependencies. (4) POD (Plain Old Documentation) inline with the code, `=head1 NAME`, `=head1 SYNOPSIS`, `=head1 METHODS`. (5) Tests in `t/` using `Test::More` or `Test2::V0`, run with `prove -lr t/`. (6) Strict + warnings (or `use v5.36`) at the top, with a minimum Perl version declared. (7) An object system if needed: Moo for libraries (light dep footprint), Moose for applications. (8) CI via GitHub Actions running on Perl 5.16, 5.20, 5.24, 5.30, 5.36, blead. Publish with `dzil release` (Dist::Zilla) or, for simple modules, plain `cpan-upload`. A real example: dependencies are pinned to minimum versions, not exact, CPAN convention is 'works on this or newer'.

Two 2026-specific notes. Perl 5.40 added the `module_true` feature, so under `use v5.40;` a module no longer needs the traditional trailing `1;`, while every older toolchain still expects it, which is why most published distributions keep the `1;` for now. And `$VERSION` has to be assignable in a form ExtUtils::MakeMaker can extract by reading the file, meaning a single plain `our $VERSION = '1.02';` line, because the PAUSE indexer parses your source and never executes it.

Add a `t/00-load.t` that simply `use_ok`s every module, measure coverage with `cover -test` from Devel::Cover, and keep author-only checks such as Test::Pod and Test::Pod::Coverage in `xt/` so that somebody installing from CPAN never fails on your style rules. Namespace permissions on PAUSE are first come first served, so check metacpan before claiming one, and use `Dist::Zilla` only if the boilerplate it removes is worth the indirection it adds for contributors.

# lib/My/Org/Widget.pm
package My::Org::Widget;
use v5.36;
our $VERSION = '1.02';   # one plain line, the indexer parses it as text

sub new ($class, %args) {
    return bless { name => $args{name} // 'widget' }, $class;
}

sub name ($self) { $self->{name} }

=head1 NAME

My::Org::Widget - small example distribution

=head1 SYNOPSIS

    use My::Org::Widget;
    my $w = My::Org::Widget->new(name => 'gear');
    print $w->name;

=head1 METHODS

=head2 name

Returns the widget name.

=cut

1;   # can be dropped under 'use v5.40' (module_true feature)

# t/01-basic.t
use Test2::V0;
use My::Org::Widget;
is(My::Org::Widget->new(name => 'gear')->name, 'gear', 'name accessor');
done_testing;

# prove -lvr t/        run tests
# cover -test          coverage report

Key Points

  • Moo over Moose unless you need MOP introspection
  • Inline POD documentation, tested with podchecker
  • cpanfile + cpanm for dev, Carton for app deploy
  • Test::More or Test2::V0 for tests
  • CI matrix across multiple Perl versions
Q33

How do you profile and optimize slow Perl code?

AdvancedPerformance

Answer

Profiling in order of preference: (1) `Devel::NYTProf` is the gold standard, produces a beautiful HTML report with per-line, per-sub, and per-block timing including subroutine call graphs. Run `perl -d:NYTProf script.pl` then `nytprofhtml`. (2) `Devel::Profile` for simpler use cases. (3) `Benchmark::timethese` for comparing alternative implementations of a hot path. Common optimizations once you've found the hotspot: cache regexes with `qr//` and reuse (compiling the regex on every call is a classic 10× win), avoid `eval` in hot paths (it's expensive), use `JSON::XS` instead of `JSON::PP`, prefer hash lookups over linear array scans, use `pack`/`unpack` for binary data instead of substr.

For genuine speed sensitivity rewrite the hot path in C (`Inline::C`) or call a CPAN XS module. At the architecture level, switch to PSGI/Plack instead of CGI to avoid per-request interpreter startup, this alone is a 10-100× improvement for web apps. Two things separate a real answer from a list of module names.

NYTProf adds roughly two to five times overhead, so profile a representative workload rather than live traffic, and when you must attach to a running service start it disabled using the `start=no` option in the `NYTPROF` environment variable and call `DB::enable_profile()` around the suspect code path. For a Plack app, profile with a single worker, because otherwise the per-process output files interleave and the report is meaningless. Second, resist micro-optimisation until the profile says otherwise: in practice the hotspot is almost always an N+1 database query inside a loop or an O(n squared) `grep` nested in a `for`, and replacing that with one query plus a hash index beats every regex tweak put together. When the profile genuinely points at Perl-level work, the reliable wins are hoisting `qr//` out of loops, `Memoize` for pure functions, `Text::CSV_XS` and `JSON::XS` in place of their pure-Perl equivalents, `substr` used as an lvalue instead of rebuilding strings, and turning off autoflush on a handle you write to in a tight loop.

# Profile a script:
perl -d:NYTProf my_script.pl
nytprofhtml --open

# Common win: precompile regex outside hot loop
use v5.36;
my $email_re = qr/^[\w.+-]+\@[\w.-]+\.[a-z]{2,}$/i;   # compile once

for my $email (@huge_list) {
    next unless $email =~ $email_re;                    # reuse compiled regex
    # ...
}

# Bench two approaches:
use Benchmark qw(cmpthese);
cmpthese(-2, {
    regex   => sub { $string =~ /pattern/ },
    index   => sub { index($string, 'pattern') >= 0 },
    substr  => sub { substr($string, 0, 7) eq 'pattern' },
});
Q34

How do you build a modern web app in Perl? (Mojolicious vs Catalyst vs Dancer)

AdvancedWeb Frameworks

Answer

Three serious contenders in 2026. **Mojolicious** is the modern, batteries-included choice: zero non-core dependencies, async out of the box, built-in WebSockets, template engine, JSON, user agent, test helpers. It's the framework most actively developed in 2026 and the recommendation for any new Perl web project. **Dancer2** is the 'Perl Sinatra', minimalist, route-centric, easy for small APIs and dashboards. **Catalyst** is the elder statesman, full MVC, mature, but heavy and slower-moving; mostly relevant if you're maintaining an existing Catalyst app. All three run under PSGI (Perl's WSGI equivalent), so they deploy behind Starman, Twiggy, or uWSGI.

The legacy giants, Bugzilla, Request Tracker, cPanel, are CGI / mod_perl 1, which is what you'll see in Indian enterprise environments. For greenfield Perl web work: Mojolicious. Honest take: most teams building new web services in 2026 should consider whether Python/Node/Go isn't a better fit unless they have a strong Perl reason (legacy integration, regex-heavy text work, a Perl-fluent team).

Deployment specifics matter in the follow-up. PSGI apps run under Starman (preforking, the safe default when your code makes blocking DBI calls), Gazelle (a faster preforker), or Twiggy (a single-process event loop, correct only if every I/O call in the stack is non-blocking). Mojolicious ships hypnotoad, which does zero-downtime restarts by starting fresh workers before retiring the old ones when it receives `USR2`.

The failure everybody hits once: a blocking DBI call inside a Mojolicious non-blocking handler stalls that entire worker along with every other connection it was serving, so either go non-blocking end to end with Mojo::Pg and promises, or run prefork and stay blocking deliberately. On the legacy side, CGI.pm was removed from core in Perl 5.22, so a twenty-year-old CGI script now needs the CGI distribution installed explicitly, and discovering that is a common first day on a legacy Perl job.

# Mojolicious::Lite, a complete web app in one file:
use Mojolicious::Lite -signatures;

get '/hello/:name' => sub ($c) {
    $c->render(json => { greeting => 'Hello, ' . $c->param('name') });
};

post '/users' => sub ($c) {
    my $user = $c->req->json;
    return $c->render(json => { error => 'name required' }, status => 422)
        unless $user->{name};
    # ... save ...
    $c->render(json => { id => 123, %$user }, status => 201);
};

websocket '/echo' => sub ($c) {
    $c->on(message => sub ($c, $msg) { $c->send("echo: $msg") });
};

app->start;

# Run: morbo app.pl  (dev) or hypnotoad app.pl (prod)
Q35

How do you handle concurrency in Perl? Threads vs forks vs async (IO::Async/Mojo::IOLoop)?

AdvancedConcurrency

Answer

Perl gives you three concurrency models, and they have very different tradeoffs. **`fork()`** is the classic Unix model, `Parallel::ForkManager` is the go-to module, gives you process-level isolation and is the safest choice for CPU-bound work. Memory isn't shared, IPC is via pipes/sockets. **`threads`** (ithreads) is Perl's threading model, but it's a 'copy everything on creation' model that's heavier than threads in most other languages, community advice in 2026 is to use forks unless you have a specific reason. **Async/event loop** via Mojo::IOLoop or IO::Async is the modern choice for I/O-bound work: one process, callbacks/promises, scales to thousands of concurrent connections. Mojolicious uses Mojo::IOLoop internally.

For typical Indian production use cases: parallel processing of a million log files = `Parallel::ForkManager` with `nproc` workers; a high-concurrency HTTP service = Mojolicious + hypnotoad with prefork; mixed I/O + CPU = a hybrid (fork worker pool that runs an event loop inside each worker). Two facts get asked directly. Check whether your Perl even supports threads with `perl -V:useithreads`, because plenty of distro builds are compiled without them, and each ithread gets its own copy of the interpreter and all data, which is why it costs more than a process does on Linux.

And copy-on-write after `fork` saves less memory than people expect: touching a Perl variable updates its reference count, that write dirties the page, and the page is copied, so a preforked worker's shared pages erode steadily. Load your data and compile your regexes before forking so the parent has already paid for them, then keep child access as read-only as you can. Other practicalities worth naming: reap children or set `$SIG{CHLD} = 'IGNORE'` so you do not accumulate zombies, never inherit a DBI handle across a fork, use `Mojo::IOLoop::Subprocess` to push a blocking job out of an event loop, and reach for `Future::AsyncAwait` when you want async and await syntax instead of nested callbacks.

use v5.36;
use Parallel::ForkManager;

# Process 1000 files in parallel, 8 workers at a time:
my $pm = Parallel::ForkManager->new(8);

$pm->run_on_finish(sub ($pid, $exit, $ident, $signal, $core, $data) {
    say "$ident finished with $data->{rows} rows";
});

for my $file (@files) {
    $pm->start($file) and next;       # parent continues
    # ── child code ──
    my $rows = process_file($file);
    $pm->finish(0, { rows => $rows });
}
$pm->wait_all_children;

# Async I/O, fetch 100 URLs concurrently with Mojo::UserAgent:
use Mojo::UserAgent;
use Mojo::Promise;

my $ua = Mojo::UserAgent->new;
my @promises = map { $ua->get_p($_) } @urls;
Mojo::Promise->all(@promises)->then(sub {
    for my $tx (map { $_->[0] } @_) {
        say $tx->result->code, ' ', $tx->req->url;
    }
})->wait;
Q36

How do you call Perl from other languages or call C from Perl?

AdvancedInterop

Answer

**Perl from other languages:** the lowest-friction option is shelling out, every language can run `perl -e '...'` or invoke a Perl script. For tighter integration, embed the Perl interpreter via `libperl` (C/C++), this is how mod_perl, Apache::ASP and several email scanners work, but it's complex and modern projects almost always use a wire protocol (JSON-RPC over HTTP, gRPC) between a Perl service and the consuming app instead. **C from Perl:** three options in increasing complexity. (1) `Inline::C`, write C inline in your Perl file, compiled on first run, cached. Perfect for one or two hot functions. (2) `XS`, the traditional C-extension mechanism; verbose but maximum control. (3) `FFI::Platypus`, call functions in a pre-existing shared library (.so / .dll) without writing any C.

FFI::Platypus is what you want if there's already a C library you want to use; XS is what CPAN-distributed XS modules use; Inline::C is the simple in-house option. For Indian enterprise legacy systems, `XS` knowledge is still occasionally interview-relevant for Oracle integration and high-frequency log processing. Two practical warnings close this out. `Inline::C` compiles on first run and caches into an `_Inline` directory beside the script, so on a read-only filesystem or an immutable container image you must set the `DIRECTORY` option or precompile with `Inline::Module`, otherwise the first production request dies with a permission error rather than a compile error you saw in testing.

And an embedded Perl interpreter is not safe to share between OS threads, so you need one interpreter per thread with `PERL_SET_CONTEXT` or a lock around all access. For XS itself, the pieces you should be able to name are the `.xs` source, the typemap that converts C types to and from Perl SVs, `ExtUtils::ParseXS`, and `PERL_NO_GET_CONTEXT` for performance on threaded builds. The pragmatic 2026 answer to most integration questions is still a wire protocol: JSON over a pipe or HTTP is easier to test, deploy and debug than an FFI boundary, and saying that first shows judgement before you demonstrate that you can do the hard version.

# Inline::C, write C inline in Perl:
use v5.36;
use Inline C => <<'END';
int fib(int n) {
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}
END

say fib(30);   # fast C version

# FFI::Platypus, call a system library without writing C:
use FFI::Platypus 2.00;

my $ffi = FFI::Platypus->new(api => 2);
$ffi->lib(undef);                       # libc
$ffi->attach(getpid => [] => 'int');
$ffi->attach(strlen => ['string'] => 'size_t');

say 'pid: ', getpid();
say 'len: ', strlen('hello, world');
Q37

How do you handle Unicode and character encoding correctly in Perl?

AdvancedUnicode

Answer

The model is: decode on input, work in characters internally, encode on output. Every Perl encoding bug is a violation of one of those three steps. `use utf8;` does none of them, it only tells the parser that the source file itself is UTF-8, which affects string literals and identifiers and nothing else. Decoding is `Encode::decode('UTF-8', $bytes)`, or better, a PerlIO layer on the handle: `open(my $fh, '<:encoding(UTF-8)', $file)`.

Know the difference between `:encoding(UTF-8)` and `:utf8`: the first validates and complains about malformed input, the second just flips a flag and trusts you, which is how invalid bytes get inside strings and detonate three functions later. `use open qw(:std :encoding(UTF-8));` sets the default for STDIN, STDOUT, STDERR and every `open` in that lexical scope. The diagnostic everybody meets is `Wide character in print`, which means you sent a character above U+00FF to a byte-oriented handle: Perl emitted UTF-8 anyway and warned you. Double encoding is the other symptom, where Devanagari or accented text renders as mojibake because it was encoded twice.

Practical consequences: `length` counts characters after decoding and bytes before, `lc`, `uc` and `\w` need `/u` semantics to behave, and equality needs `Unicode::Normalize` first because the same visible character can have two code point sequences. On MySQL, the charset named `utf8` is a three-byte subset, so you need `utf8mb4` with `mysql_enable_utf8mb4 => 1` or DBD::MariaDB, otherwise emoji and some Indic sequences truncate the column.

use v5.36;
use utf8;                                # THIS FILE is written in UTF-8
use open qw(:std :encoding(UTF-8));      # STDIN/STDOUT/STDERR and all opens
use Encode qw(decode encode);
use Unicode::Normalize qw(NFC);

my $name = 'नमस्ते';
say length $name;                        # characters (thanks to use utf8)
say length encode('UTF-8', $name);       # bytes, a bigger number

# Validating layer versus trusting layer
open(my $ok, '<:encoding(UTF-8)', 'in.txt') or die $!;   # complains on bad bytes
# open(my $bad, '<:utf8', 'in.txt');     # accepts malformed input silently

# Raw bytes from a socket or a binary read must be decoded explicitly
my $raw   = do { open my $r, '<:raw', 'in.txt' or die $!; local $/; <$r> };
my $chars = decode('UTF-8', $raw);

# Compare only after normalising, two encodings can look identical
my $other = decode('UTF-8', encode('UTF-8', $name));
say 'same' if NFC($name) eq NFC($other);

# DBI: utf8mb4, not MySQL's 3-byte 'utf8'
use DBI;
my $dbh = DBI->connect($ENV{DSN}, $ENV{DB_USER}, $ENV{DB_PASS},
    { mysql_enable_utf8mb4 => 1, RaiseError => 1 });

Key Points

  • use utf8 describes the source file, not your I/O
  • :encoding(UTF-8) validates, :utf8 only sets a flag
  • 'Wide character in print' means an undecoded output handle
  • MySQL utf8 is 3-byte, use utf8mb4
Q38

What changed between Perl 5.36 and 5.42, and which of it can you actually use at work?

AdvancedLanguage Versions

Answer

5.36 (2022) is the practical baseline. Subroutine signatures became stable, and `use v5.36;` enables strict, warnings, `say`, signatures and `isa` while switching off indirect object syntax and multidimensional array emulation. It also introduced the `builtin` namespace (`true`, `false`, `is_bool`, `trim`, `ceil`, `floor`, `indexed`, `blessed`, `refaddr`, `reftype`, `weaken`) as experimental, `defer { }` blocks that run on scope exit, and the two-variable `for my ($k, $v) (%hash)` form. 5.38 (2023) added the built-in object system: `class`, `field`, `method`, `ADJUST` and the `:param` attribute, experimental but usable. 5.40 (2024) is the release worth arguing for at work: `try`/`catch` stopped being experimental, fields gained `:reader` to generate accessors, `__CLASS__` returns the invocant's class inside a method, `^^` provides a logical xor, and the `module_true` feature means a module no longer needs a trailing `1;`. 5.42 (2025) stabilised much of `builtin`, added `any` and `all` as keywords, dropped the ancient apostrophe package separator, and moved to a newer Unicode release.

What you can actually use is decided by the interpreter you deploy on, and distro Perls lag badly: RHEL 9 ships 5.32, Debian 12 ships 5.36 and Ubuntu 24.04 ships 5.38. That makes `use v5.36;` the safe floor for code that runs on customer machines, with perlbrew or a container when you want more, and it is why Moo remains the sensible object system until your minimum interpreter clears 5.38.

use v5.40;   # strict, warnings, say, signatures, try/catch, module_true
use feature 'defer'; no warnings 'experimental::defer';
use JSON::PP qw(decode_json);
use Path::Tiny qw(path);

# try/catch: no longer experimental as of 5.40, no module needed
sub read_config ($path) {
    try   { return decode_json(path($path)->slurp_raw) }
    catch ($e) { warn "config unreadable: $e"; return {} }
}

# defer: runs on scope exit, including on die
sub with_lock ($file, $code) {
    open my $fh, '>', "$file.lock" or die $!;
    defer { unlink "$file.lock" }
    return $code->();
}

# Two-variable foreach (5.36+)
my %hits = (home => 40, jobs => 120);
for my ($page, $n) (%hits) { say "$page: $n" }

# Built-in class syntax: 5.38+, :reader added in 5.40
use experimental 'class';
class Point {
    field $x :param :reader = 0;
    field $y :param :reader = 0;
    method as_string { sprintf '%s(%d,%d)', __CLASS__, $x, $y }
}
say Point->new(x => 3, y => 4)->as_string;   # Point(3,4)
💡 Pro Tip: Check the target box with `perl -v` before promising signatures or try/catch, RHEL 9 is still on 5.32.
Q39

A long-running Perl daemon's memory keeps growing until the OOM killer takes it. How do you find the cause?

AdvancedProduction Debugging

Answer

First separate a leak from a high-water mark. Perl returns freed scalars to its own arenas rather than to the operating system, so resident memory is a high-water mark by design: one request that slurps a 500 MB file permanently raises RSS even though nothing leaked. Sample RSS per minute and look at the shape.

A plateau means you sized something badly, a climb proportional to traffic means a real leak. The usual causes, roughly in order of frequency: reference cycles, where an object holds children that hold a back-link to the parent, or a callback closing over the invocant that is then stored on the invocant, because reference counting can never reclaim those; unbounded caches, including a memoisation hash with no eviction; autovivification writing a new key into a package-level hash on every lookup of an unknown id; stashing whole result sets from `fetchall_arrayref`; and runtime `require` per request, which grows `%INC` and the optree. Tools, in the order you should reach for them: `Devel::Cycle`'s `find_cycle($ref)` prints the exact path of a cycle, `Devel::Gladiator` walks every live SV so you can diff counts by type between two points, and `Devel::MAT` is the real production answer, dumping the entire heap to a `.pmat` file from a signal handler for offline analysis with the `pmat-*` tools. `Devel::Size::total_size` helps but is slow and double counts shared structures. Fix cycles with `Scalar::Util::weaken`, bound every cache, and meanwhile set a per-worker request limit in Starman or hypnotoad so processes recycle before they die.

use v5.36;
use Scalar::Util qw(weaken);
use Devel::Cycle qw(find_cycle);

package Node {
    sub new ($class, $name) { bless { name => $name, kids => [] }, $class }
    sub add ($self, $kid) {
        push @{ $self->{kids} }, $kid;
        $kid->{parent} = $self;      # strong back-link = cycle = never freed
        weaken($kid->{parent});      # the fix
    }
}

my $root = Node->new('root');
$root->add(Node->new('child'));
find_cycle($root);                   # prints the cycle path, silent once weakened

# Same trap with a callback stored on the object it closes over
my $worker = { name => 'ingest' };
my $weak   = $worker; weaken($weak);
$worker->{on_tick} = sub { say $weak->{name} };

# Heap dump on demand in production:
#   use Devel::MAT::Dumper;
#   $SIG{USR2} = sub { Devel::MAT::Dumper::dump("/tmp/heap.$$.pmat") };
#   kill -USR2 <pid>  then:  pmat-leakreport /tmp/heap.NNNN.pmat

Key Points

  • Perl never returns freed memory to the OS, RSS is a high-water mark
  • Reference cycles are the number one leak, fix with Scalar::Util::weaken
  • Devel::MAT dumps a real heap you can analyse offline
  • Recycle workers as mitigation, not as the fix
Q40

How do you write secure Perl? Taint mode, injection, and the idioms to avoid.

AdvancedSecurity

Answer

Taint mode is the language-level control: run with `perl -T` and Perl marks everything originating outside the program (arguments, environment, file input, sockets) as tainted, then refuses to let that data reach anything touching the system, including `system`, `exec`, backticks, `open` for writing, `unlink`, `chmod` and `eval`. It also clears `PATH`, `IFS`, `CDPATH` and `BASH_ENV`, so you must set `$ENV{PATH}` yourself before shelling out. The only way to untaint is to validate with a regex and take the capture, `my ($safe) = $input =~ /^([\w.-]+)$/ or die 'bad input';`, which forces you to state what you consider legal rather than what you consider dangerous.

Note that `-T` must appear on the real command line or the actual shebang, otherwise Perl aborts with `Too late for -T option`. Beyond taint, the idioms that cause incidents: two-argument `open($fh, $file)` interprets the filename, so a value like a redirect prefix truncates a file and one ending in a pipe character executes a command, which is exactly why three-argument `open` exists; `system` with one interpolated string goes through a shell while the list form does not; backticks always use a shell, so prefer `IPC::Run3`; SQL always uses placeholders; and string `eval` on anything user-influenced is remote code execution. Two Perl-specific ones worth naming: never `Storable::thaw` or load YAML from an untrusted source, since both can instantiate arbitrary objects, and never interpolate raw user input into a pattern, both because of catastrophic backtracking and because patterns can carry embedded code constructs.

#!/usr/bin/perl -T
use v5.36;
use IPC::Run3;

$ENV{PATH} = '/usr/bin:/bin';              # taint mode clears it, set it yourself
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};

# Untaint by validating and capturing, never by assertion
my $raw = $ARGV[0] // '';
my ($host) = $raw =~ /^([A-Za-z0-9.-]+)$/
    or die "refusing untrusted hostname: $raw\n";

# List form: no shell involved, so nothing to inject into
run3 ['/usr/bin/dig', '+short', $host], undef, \my $out, \my $err;
die "dig failed: $err" if $?;

# 2-arg open is a command-execution hole:
#   open(my $fh, $file);   # $file = 'rm -rf /tmp/x|'  runs a command
open(my $fh, '<', "$host.txt") or die $!;   # 3-arg form is safe

# SQL: placeholders, never interpolation
use DBI;
my $dbh = DBI->connect($ENV{DSN}, $ENV{DB_USER}, $ENV{DB_PASS}, { RaiseError => 1 });
my $sth = $dbh->prepare('SELECT ip FROM hosts WHERE name = ?');
$sth->execute($host);

# Quote user input before it reaches a pattern
my $needle = quotemeta $raw;
say 'found' if $out =~ /$needle/;
💡 Pro Tip: `Too late for -T option` means you ran `perl script.pl` instead of `./script.pl`, taint has to be set at interpreter startup.

Companies Hiring Perl

Booking.com
IBM
Cisco
Wipro
TCS
Infosys
cPanel
DuckDuckGo

Salary Insights

Average in India
₹5-15 LPA

Frequently Asked Questions

Is Perl dead in 2026?

No, but it's not growing either. Perl is in the 'COBOL phase' of its life cycle, enormous installed base that nobody's rewriting, low new-project adoption, stable jobs maintaining legacy. Booking.com, Bugzilla, Request Tracker, cPanel, and most Indian banking/telecom legacy backends still run Perl. The honest summary: don't pick Perl for a greenfield project in 2026 unless you have a specific reason (regex-heavy text processing, existing Perl team, integrating with a Perl-heavy environment). But Perl knowledge remains valuable, there's still good money in maintaining the systems that already exist.

How much does a Perl developer earn in India?

₹5-15 LPA in 2026, with the floor higher than Python equivalents because the pool of capable Perl developers has shrunk. Senior Perl + DBA + Linux admin combinations at Indian banks, telecom (Airtel, Jio backend bits), and BPO operations (Wipro, TCS, Infosys legacy contracts) can go ₹12-18 LPA. Booking.com's Bangalore office historically paid significantly above market for Perl talent. Specialised areas (bioinformatics, Perl on Wall Street back-office systems) pay at the upper end. What moves you up the band is rarely more Perl: it is Perl plus SQL tuning, plus Linux and shell, plus willingness to own the nightly batch window and its on-call pager. Roles advertised as 'Perl developer' pay less than the same work advertised as 'platform engineer, legacy systems', so read the JD before anchoring your number.

How long does it take to prepare, and what is different for freshers versus experienced candidates?

If you already program in another language, two to three weeks of evenings covers a maintenance role: context, references, regex, DBI, and reading unfamiliar code fluently. Freshers are asked to explain sigils and context out loud, write a split-based log parser live, and produce a one-liner for something like counting the top IPs in an access log, so practise typing Perl rather than reading about it. Experienced candidates get almost no syntax questions. That interview is about a system you actually maintained: where transactions and error handling live, how you tracked down a memory or performance problem in a long-running process, and whether you can argue honestly about migrating a legacy codebase rather than reflexively proposing a rewrite. Expect a code-reading exercise on twenty-year-old code with no signatures and no strict, not a whiteboard algorithm round. Add a week if you have never touched Moo, Moose or Mojolicious, since that is the usual gap for people who only know legacy Perl.

Should I learn Perl in 2026?

If you're starting your career, learn Python or JavaScript first, that's where the jobs are. Learn Perl as a second or third language if you (a) need it for a specific job, (b) work in bioinformatics or system administration, or (c) want to be able to read and maintain legacy systems. Perl 5.36+ is genuinely pleasant to write and the regex / one-liner skills transfer to every other language you'll ever use.

When does Perl still beat Python in 2026?

Three real cases. (1) One-liners and short text-processing scripts, `perl -lane '...'` is shorter and faster than the Python equivalent. (2) Heavy regex work, Perl's regex engine is the reference implementation and has features (recursive patterns, regex variables) Python's `re` module doesn't have natively. (3) Environments where Perl is pre-installed and you can't add new dependencies, most Linux distros, most Unix servers, many embedded systems. For everything else (web apps, ML, data science, modern CLI tools), Python has the better ecosystem in 2026.

What's the difference between Perl 5 and Perl 6/Raku?

Raku (renamed from Perl 6 in 2019) is a separate language that started as a Perl 5 successor but diverged significantly. They share aesthetics but not code, Raku is its own language with its own community and toolchain. When someone says 'Perl' in 2026 they mean Perl 5.36+, full stop. Raku has its own niche but isn't on the typical Indian enterprise stack.

Introduction

Perl is the language people love to declare dead and then keep using. In 2026 it still powers an enormous amount of production infrastructure: Booking.com's main application, Bugzilla, Request Tracker, cPanel, large slabs of legacy enterprise web, and the glue layer of countless DevOps pipelines. Indian banking back-offices, telecom OSS/BSS at Airtel and Jio, and BPO shops at Wipro/TCS still run substantial Perl codebases that nobody is in a hurry to rewrite.

Interviews for Perl roles in India in 2026 split into two camps. The 'maintain legacy systems' camp tests your ability to read 20-year-old code: contexts, references, $_, regex modifiers, symbol tables. The 'modern Perl' camp (Booking.com, smaller dev shops) tests what landed between Perl 5.36 and 5.42: native signatures, postfix dereferencing, the `builtin` namespace, `try`/`catch`, the new `class` feature, Moose/Moo OOP, and CPAN literacy.

This guide covers 40 Perl interview questions asked in 2026, ordered basic, then intermediate, then advanced. Each answer covers how the feature actually behaves, the gotchas that bite in production (and Perl has many), version differences that matter because distro Perls lag, and a code example where it pays for itself. Honest note throughout: Python has eaten most of Perl's lunch for new projects, but Perl still wins for one-liners, hardcore regex, and any environment where 'Perl is already installed and works' beats 'install a new toolchain'.

Ready to practice Perl interviews?

Don't just read, practice these Perl questions live with an AI interviewer that asks follow-ups and scores your answers.

AI-powered practice
Instant feedback
Free to start
Start Free Mock Interview