Perl standards (reference: August 2026)
Criteria verified as of August 2026. Re-verify on the web before committing to anything (§8).
1. Scope and triggers
The real Perl case today is living legacy: 10- or 20-year-old code that brings in revenue, that
nobody wants to touch and that has no tests. Most of the work here is maintaining, containing and
deciding, not creating. This document is written from there: first do not break it, then improve
it, and only with explicit criteria rewrite it. Perl is not the answer for new code unless the team
already maintains it (§7).
Triggers: .pl, .pm, .t, .psgi, cpanfile, cpanfile.snapshot, Makefile.PL, Build.PL,
.perlcriticrc, .perltidyrc, dist.ini, cpanm, carton, perlbrew, plenv, prove,
Moose/Moo/Object::Pad, DBI, Mojolicious, Dancer2, -T.
Not applicable: see bash-linux-scripting-standards (a reciprocal and very real boundary:
Perl was the historical successor to the shell when a script got too clever, and that rule is still
alive only in reverse — today the destination of a growing shell script is python-standards,
not Perl. A .sh that needs data structures does not become a .pl), python-standards (the
natural destination of a rewrite: when §7 says "rewrite", that is where it means),
linux-administration-standards and rhel-fedora-standards (the system Perl, its RPM/DEB packages
and the OS tools that depend on it), lua-standards and groovy-standards (nothing in common),
appsec-standards (methodology; here only Perl's concrete sinks), sql-standards (the SQL that goes
through DBI), vulnerability-management-standards, secrets-management-standards,
observability-standards.
2. Default decisions: interpreter and dependencies
Verify the latest version on the web before pinning it in a real project (§8).
Versions and support (perlpolicy, verified as of Aug 2026):
| Series |
Status |
| 5.44.x |
Current stable, released on 2026-07-15. Full support |
| 5.42.x |
Previous stable (since 2025-07-03). Full support |
| 5.40.x |
Since 2024-06-09. Security fixes only |
| 5.45.x |
Development series (odd X). Never in production |
Policy quoted verbatim: "To the best of our ability, we will provide 'critical' security patches /
releases for any major version of Perl whose 5.x.0 release was within the past three years."
Numbering: even X = stable, odd X = development.
"Perl 7" does not exist and is not going to exist as announced. The history is necessary because
it still causes confusion: it was announced in June 2020 (Sawyer X) as "Perl 5 with modern defaults";
rejection over process and over backward compatibility stopped it, it led to a governance crisis that
produced perlgov and the Perl Steering Council, and the original idea died. The 5.x series
continues without interruption; perlpolicy as of Aug 2026 does not mention Perl 7. If someone is
planning around "when Perl 7 comes out", the plan is wrong.
FORBIDDEN to touch the system Perl. On RHEL/Fedora and Debian/Ubuntu the interpreter and its
modules are part of the OS: they are used by the package manager, boot tooling, debconf and
maintenance scripts. Installing modules with cpan/cpanm as root over /usr/lib/perl5, or
replacing the binary, breaks the operating system and leaves a machine that is neither
reproducible nor patchable. Rules:
- Modules the OS needs → distro package (
perl-*), never CPAN over the system tree.
- Your own applications → their own interpreter with
perlbrew or plenv, or at least an
isolated module tree with local::lib. Each application with its own tree; never shared.
- In a container: an image with its Perl and its
cpanfile.snapshot; no cpanm at runtime.
Dependencies:
| Piece |
Choice |
Verified (MetaCPAN, Aug 2026) |
| Installer |
cpanm (App-cpanminus) |
1.7049 (2026-03-17) |
| Declaration |
cpanfile with bounded ranges |
— |
| Lock + reproducibility |
Carton (cpanfile.snapshot, carton install --deployment) |
v1.0.35, 2022-05-07 |
| Isolation |
local::lib |
2.000029, 2022-04-20 |
| Formatting |
perltidy |
20260705, GPL-2.0 licence (not "same as Perl") |
| Lint |
Perl::Critic |
1.156 (2024-10-23) |
| Tests |
Test2::V0 + prove / yath |
see §8 (declared discrepancy) |
| Coverage |
Devel::Cover |
1.52 (2026-03-07) |
| Auditing |
CPAN::Audit / cpan-audit |
20260622.001 (2026-06-22) |
Carton and local::lib have gone years without a release: they work and they are the de facto
standard, but they are not actively maintained software — a fact to declare, not to hide. Carmel
is the same author's alternative and is even more stalled. Criterion: cpanfile +
cpanfile.snapshot committed, and reproducible installation from the snapshot; if the team can
afford it, freezing the module tree in the container image is more robust than trusting a resolution
from CPAN on every build.
3. The language: line zero and what breaks
Line zero, non-negotiable, in every .pl, .pm and .t file:
use strict;
use warnings;
Without strict no refactor is possible: a typo silently creates a new global symbol. It is the
difference between "maintainable Perl" and "Perl of legend". Acceptable alternative:
use Modern::Perl or use v5.36; or above — use VERSION with 5.36+ enables strict, warnings
and signatures automatically and is the preferred form in new code. Migration warning: Perl 5.40
warned that repeated use VERSION declarations stop being allowed in 5.44.
- Sigils and context are the number one source of bugs.
$x, @x and %x are different
variables, and the sigil indicates what you get, not what the variable is ($array[0],
$hash{k}). Above all: scalar vs list context. my ($x) = f(); takes the first element of the
list; my $x = f(); puts the function in scalar context (for an array, its length). An empty list
gives undef in scalar context and disappears when interpolated into another list. Rule: every
function returning collections returns a reference (\@result), except with a deliberate,
documented list contract. It is the only way to stop context biting you.
- References: nested structures only with references (
$h->{a}[0]{b}). Beware
autovivification: reading $h->{a}{b} creates $h->{a}; use exists if the creation matters.
my / our / local: my is lexical and covers 99%. our is an alias to the package: only
constants and $VERSION. local is not local: it is a temporary dynamic value over a global
(local $/;); necessary for special variables, poison for anything else.
- Object systems, explicit criteria:
- Native
class in the core: it has existed since 5.38 (the "Corinna" project), and it is
still experimental as of 5.44 (warnings in the experimental::class category; the 5.44
perldelta adds an entry in perlexperiment for "New object system and class syntax").
Operational translation: it is the future and can already be used in an internal project that
controls its own interpreter, but not in a CPAN distribution nor in a system that cannot absorb a
syntax change.
Object::Pad (0.825, 2026-03) is the laboratory class came out of, actively maintained: the
bridge if you want that syntax today and accept following its changes.
Moo (2.005005, 2023-01): lightweight, no XS, fast startup; stable but without a release
in years. Moose (2.4000, 2025-07): complete (roles, type constraints, MOP) with a real
startup and memory cost — only if you already use it or genuinely need the MOP.
- Criterion: a codebase already using Moose → Moose. A new, controlled codebase → native
class
accepting its experimental status, or Moo if you want stability today. Never hand-written
bless in new code, and never two object systems in the same project.
- Errors:
eval { }; if ($@) { } is correct in a subtle and fragile way. Real traps: $@ can
be clobbered by the destructor of an object released on leaving the eval; $@ false but with an
error having occurred if the exception was the string "0"; and eval without a block
(eval EXPR) is something completely different. Use Try::Tiny (or feature 'try' if your
interpreter has it and you accept its status) and always check the return value of eval, not
just $@. Exception objects (your own class or Throwable) over strings: an error that is only
text can neither be classified nor retried.
- Regular expressions: Perl's strong point and its biggest risk.
- Precompile with
qr// anything used in a loop; use /x in any pattern longer than one line (with
comments) — a dense regex without /x is unreviewable code.
- Names, not numbers:
(?<name>...) and $+{name}. $1, $2… break when you reorder.
- ReDoS is real: Perl's engine does backtracking and a pattern with nested quantifiers
(
(a+)+) over user input takes the CPU with it. Over untrusted input: bound the subject's length,
avoid nested quantifiers and overlapping alternations, prefer negated character classes to .*,
and never accept a user-supplied pattern.
/e (evaluates the replacement as code) over external data is eval STRING in disguise: vetoed.
4. Quality: formatting, lint, tests
- Formatting:
perltidy with a committed .perltidyrc; perltidy -b locally and perltidy in
check mode as a gate. No style debates.
- Lint:
Perl::Critic with .perlcriticrc in the repo. Severity criteria:
- New code or an already cleaned-up module: severity 3 (
--severity 3, "harsh") as the gate.
- Legacy entering the pipeline for the first time: severity 5 (
gentle) as the gate, dropping
one level per quarter. Setting severity 1 on a 100k-line legacy codebase does not produce quality,
it produces a ## no critic in every file and the abandonment of the linter.
- Mandatory policies whatever the severity:
RequireUseStrict, RequireUseWarnings,
ProhibitTwoArgOpen, ProhibitStringyEval, ProhibitBacktickOperators.
## no critic (PolicyName) always with the policy named and a reason; never bare.
- Warning: Perl::Critic has not published a release since 2024-10 (1.156) — it is still the tool,
but its cadence is slow.
- Tests:
Test2::V0 for new suites; Test::More is acceptable and ubiquitous in legacy, do not migrate
it for taste. Runner: prove -lr t/ (or yath), in parallel (-j) only if the tests really
are independent.
- Cover the happy path, edges and errors: empty inputs,
undef, encoding (Perl and UTF-8 is a
minefield: decide where you decode and test with non-ASCII), DB errors, timeouts.
Devel::Cover to measure, not as a religious threshold. In legacy with no tests, useful
coverage is built from the edge: first a characterisation test that pins the current observable
behaviour (however absurd), then you touch it.
- Every bugfix leaves a regression test. An unstable test: fix it or delete it.
- CI gates (they block the merge, from cheap to expensive):
perl -c over every modified file (it compiles).
perltidy in check mode.
Perl::Critic at the agreed severity.
- Full
prove.
cpan-audit over the cpanfile.snapshot.
- The suite runs against the same interpreter version as production, installed with perlbrew/plenv
or in the container. "It passes on my machine with the system Perl" is not a signal.
5. Security
- Taint mode (
-T): enables the marking of external data (arguments, environment, files, network)
and makes its use in dangerous operations fatal; it is enabled automatically in setuid scripts.
It is still documented in perlsec as a live feature and is still Perl's only systemic safety
net for code processing external input with privileges. Criterion: enable it in any network,
CGI or privileged script; in large legacy, enable it on new entry points. Two limits you must be
aware of: (a) untainting is done with a regex capture, i.e. your pattern provides the security,
not Perl — a =~ /(.*)/s is untainting without validation and is worth nothing; (b) there is a
long-running plan to make taint support a compile-time option and perhaps retire it (see the gap in
§8): do not design an architecture whose only defence is taint.
- Command execution — the list form versus the string form.
system("cmd $x"), exec("cmd $x")
and backticks `cmd $x` pass the string through the shell: direct injection. Always use
the list form: system('cmd', $x) / open(my $fh, '-|', 'cmd', $x), which does an exec without
a shell. If you need to capture output with variable arguments, IPC::Run3/IPC::Run with an
argument list. Always check the exit status ($?): an unchecked system is a silent failure.
- Two-argument
open: FORBIDDEN. open(FH, $file) interprets metacharacters in $file: a name
starting with > writes, and one ending in | runs a command. Always three arguments with an
explicit mode and a lexical filehandle: open(my $fh, '<', $file) or die .... And always check the
return of open, close, print and unlink.
eval STRING: FORBIDDEN with any data coming from outside. eval { BLOCK } (exception handling)
is a different construct and is legitimate. Equally vetoed: /e in substitutions over external data,
and sprintf/printf with a format that comes from the user.
- Deserialisation:
Storable is unsafe over untrusted input — thaw/retrieve over data you
did not generate yourself is code execution and memory corruption; its own documentation warns about
it. Vetoed as an interchange format with third parties; acceptable only for data your own process
wrote into a trusted store. Alternatives: JSON (JSON::PP/Cpanel::JSON::XS, without blessed
objects, with max_depth/max_size), and YAML only with a safe loader (YAML::PP in safe
mode / YAML::XS with $YAML::XS::LoadBlessed = 0) — a YAML loader that instantiates objects is a
remote eval.
- SQL: only DBI placeholders (
$dbh->prepare("... WHERE id = ?") + execute($id)).
Interpolating a variable into SQL is an absolute veto; identifiers that do not accept a placeholder
(table/column names) are validated against an allowlist, never quoted by hand.
- Paths and files: canonicalise (
Cwd::realpath) and check the path is still inside the allowed
directory before opening; temporaries with File::Temp, never predictable names.
- Secrets outside the code, the
cpanfile and the logs; DBI with credentials from the
environment; a Dumper of a connection structure carries the password.
- SCA:
cpan-audit over the cpanfile.snapshot as a CI gate. CPAN has abandoned modules with no
successor: every dependency with no release in >5 years that touches network, crypto or parsing gets
reviewed.
- Cryptography: nothing home-made. Password hashing with
Crypt::Argon2 or Crypt::Bcrypt;
never crypt(), MD5 or SHA-1 for passwords. TLS with IO::Socket::SSL with certificate
verification enabled (SSL_verify_mode => SSL_VERIFY_PEER) — disabling it is a veto.
6. Performance and operability
- Before optimising, profile (
Devel::NYTProf). In Perl the cost is usually in I/O, in badly written
regexes, in DBI doing N+1 and in loading half of CPAN at startup (Moose versus Moo matters in
short-lived processes such as CGI or cron).
- Long-lived processes (PSGI/Plack, daemons): leaks from circular references — the reference
counter does not release them; break the cycle with
Scalar::Util::weaken. Watch RSS: monotonic
growth = a cycle or an unbounded cache.
- Web deployment: PSGI/Plack with Starman/uWSGI behind a proxy. CGI is kept if it already exists;
it is not chosen.
- Explicit timeouts in every HTTP client (
LWP::UserAgent, HTTP::Tiny) and in the DB; retries with
backoff only on idempotent operations.
- Structured logs (
Log::Any/Log::Log4perl with JSON output) and a correlation id; warn/die
reach the log with context. Scattered print STDERR is not observability. In a supervised process,
autoflush enabled (or you lose the last logs when it dies) and SIGTERM handled for a clean
shutdown.
- Encoding: decide the boundary (
binmode, :encoding(UTF-8), use open) once and document it;
mojibake in Perl is almost always an undeclared boundary.
7. Sustainability: maintain, contain, and when to rewrite
- Cadence: follow Perl's stable series with at most one major version jump per year, and never
leave the interpreter outside security support (a 3-year window). Upgrading the Perl of a legacy
application is a project with its own test plan, not a
perlbrew install.
- Dependencies: quarterly review with
cpan-audit; an abandoned dependency that touches the attack
surface is replaced or vendored, and its maintenance is taken on in writing.
- Document the system while you touch it: in legacy with no tests, a README with "what it does,
who calls it, what breaks if it dies" is worth more than a refactor.
When it gets rewritten and when it does not (honest criteria):
- You do not rewrite a Perl that works, is in production and has no tests. Rewriting without
tests is exchanging a known, bounded risk (ugly code that has worked for 15 years, with all its
edge cases already discovered by reality) for an unknown risk (pretty code that has not found
any of them yet). The cost is not the one estimated: the expensive part is the invisible list of
behaviours nobody documented and that somebody depends on continuing to happen.
- You contain it first: pin the interpreter, put the repo in CI, add
strict/warnings file by
file, write characterisation tests over the real inputs and outputs, isolate the module behind a
stable interface. That work has value even if you never rewrite — and it is a prerequisite if
you do rewrite, because the characterisation tests are the acceptance criteria for the rewrite.
- You rewrite when at least one hard condition holds, not an aesthetic one: nobody on the team can
maintain it and you cannot hire for it; it depends on a Perl version or on modules already out of
security support with no upgrade path; the business change requires touching the core every sprint;
or the cost of incidents already exceeds that of the rewrite, measured.
- How you rewrite: in pieces, with the old and the new coexisting behind an interface (strangler),
with the characterisation tests as the oracle, and with a declared destination — usually Python (see
python-standards). Big bang never.
List of prohibitions (veto):
- ❌ Any file without
use strict; use warnings; (or use v5.36;+). Not even one-line scripts.
- ❌ FORBIDDEN to install CPAN modules over the system Perl or replace its binary on RHEL/Debian.
- ❌ FORBIDDEN: two-argument
open.
- ❌ FORBIDDEN:
eval STRING over external data; /e with input; sprintf with a user-supplied
format.
- ❌ FORBIDDEN:
system/exec/backticks in string form with variable data. Always the list form.
- ❌ FORBIDDEN:
Storable::thaw/retrieve over untrusted input; YAML with a loader that
instantiates objects.
- ❌ SQL by interpolation;
IO::Socket::SSL without certificate verification; crypt/MD5/SHA-1 for
passwords.
- ❌ User-supplied regex patterns; nested quantifiers over unbounded input.
- ❌
## no critic without a named policy and a reason; $@ as the only failure signal from an eval.
- ❌ Hand-written
bless, or two object systems in the same project.
- ❌ Returning lists from functions that return collections (return references) without a documented
contract.
- ❌ Deploying without
cpanfile.snapshot, or installing from CPAN at service startup.
- ❌ Running the suite against a Perl version different from production's.
- ❌ Rewriting a legacy codebase without prior characterisation tests.
- ❌ Choosing Perl for new code. With one exception: the team already maintains Perl, already has
the interpreter, the experience and the internal libraries, and the new work is a natural extension
of that system. Outside that, hiring, the ecosystem and the tooling cadence play against you: the
default destination is Python.
8. Mandatory web verification
Before pinning versions or APIs, verify online (WebSearch/WebFetch; MetaCPAN
https://fastapi.metacpan.org/v1/release/<Dist> for CPAN versions and licences — for Perl modules
the source of truth is CPAN, not the GitHub releases, which are often out of date or non-existent):
perldoc.perl.org/perlpolicy: which series are supported today (as of Aug 2026: 5.44 and 5.42
full support, 5.40 security only) and whether 5.46 is already out. Never deploy an odd X series.
- The
perldelta of the target version before any series jump: 5.44 made fatal things that used to
warn (goto into a block, "Attempt to call undefined ... method") and removed repeated
use VERSION declarations.
- Status of
use feature 'class': as of Aug 2026 it is still experimental (the
experimental::class warning category); check the core tracking issue and the perlexperiment of
your version before betting on it for anything publishable.
- Versions and licences on CPAN of: Perl::Critic (1.156, no release since 2024-10), perltidy
(GPL-2.0), Carton (no release since 2022), local::lib (2022), Moo (2023-01),
Moose (2.4000), Object::Pad (0.825), Try::Tiny (MIT), Devel::Cover, CPAN::Audit.
- Vulnerabilities: an up-to-date
cpan-audit + osv.dev/GitHub Advisories for the modules with XS.
- If the project is web: version and status of Mojolicious (9.48 as of Aug 2026, Artistic-2.0),
Dancer2, DBI and its DBD.
Gaps not verified as of Aug 2026 (do not fill from memory; check before deciding):
- The real status of taint mode. It is documented as alive in
perlsec and there is a public,
long-running plan to make it a compile-time option (-Utaint_support, introduced around 5.36) and
eventually retire it. It is not verified whether the default changed in 5.42/5.44 nor whether
there is a retirement date: check it in the perldelta of your version and in the core issue
tracker before basing a design on -T.
Declared discrepancies:
- The GitHub releases feed for
Perl-Critic/Perl-Critic shows v1.154 (2024-10-21) while MetaCPAN
gives 1.156 (2024-10-23) for the same distribution. MetaCPAN wins (CPAN is the publication
channel); the GitHub feed is incomplete.
- The MetaCPAN API resolves the module
Test2::V0 to the Test-Simple distribution 1.302222
(2026-06-15), not to Test2-Suite, and the direct query for the Test2-Suite release returned no
data. It is not verified which distribution Test2::V0 lives in today: check it on MetaCPAN
before writing it into a cpanfile.
If the web contradicts this document, the web wins — flag the discrepancy.
1---2name: perl-standards3description: Use when working with Perl code - .pl/.pm/.t/.psgi files, "#!/usr/bin/perl" scripts, cpanfile, cpanfile.snapshot, Makefile.PL/Build.PL, .perlcriticrc, .perltidyrc, dist.ini, cpanm/carton/local::lib/perlbrew/plenv, Moose/Moo/Object::Pad or the native class feature, Try::Tiny and eval/$@ error handling, prove and Test2::V0 or Test::More suites, taint mode -T, DBI, Mojolicious, Dancer2, or maintaining and deciding whether to rewrite a legacy Perl codebase.4---56# Perl standards (reference: August 2026)78Criteria verified as of **August 2026**. Re-verify on the web before committing to anything (§8).910## 1. Scope and triggers1112**The real Perl case today is living legacy**: 10- or 20-year-old code that brings in revenue, that13nobody wants to touch and that has no tests. Most of the work here is **maintaining, containing and14deciding**, not creating. This document is written from there: first do not break it, then improve15it, and only with explicit criteria rewrite it. Perl is not the answer for new code unless the team16already maintains it (§7).1718Triggers: `.pl`, `.pm`, `.t`, `.psgi`, `cpanfile`, `cpanfile.snapshot`, `Makefile.PL`, `Build.PL`,19`.perlcriticrc`, `.perltidyrc`, `dist.ini`, `cpanm`, `carton`, `perlbrew`, `plenv`, `prove`,20`Moose`/`Moo`/`Object::Pad`, `DBI`, `Mojolicious`, `Dancer2`, `-T`.2122**Not applicable**: see `bash-linux-scripting-standards` (**a reciprocal and very real boundary**:23Perl was the historical successor to the shell when a script got too clever, and that rule is still24alive **only in reverse** — today the destination of a growing shell script is `python-standards`,25not Perl. A `.sh` that needs data structures does not become a `.pl`), `python-standards` (**the26natural destination of a rewrite**: when §7 says "rewrite", that is where it means),27`linux-administration-standards` and `rhel-fedora-standards` (**the system Perl, its RPM/DEB packages28and the OS tools that depend on it**), `lua-standards` and `groovy-standards` (nothing in common),29`appsec-standards` (methodology; here only Perl's concrete sinks), `sql-standards` (the SQL that goes30through `DBI`), `vulnerability-management-standards`, `secrets-management-standards`,31`observability-standards`.3233## 2. Default decisions: interpreter and dependencies3435> Verify the latest version on the web before pinning it in a real project (§8).3637**Versions and support** (`perlpolicy`, verified as of Aug 2026):3839| Series | Status |40|---|---|41| **5.44.x** | Current stable, released on **2026-07-15**. *Full support* |42| **5.42.x** | Previous stable (since 2025-07-03). *Full support* |43| 5.40.x | Since 2024-06-09. **Security fixes only** |44| 5.45.x | **Development** series (odd X). **Never in production** |4546Policy quoted verbatim: *"To the best of our ability, we will provide 'critical' security patches /47releases for any major version of Perl whose 5.x.0 release was within the past three years."*48Numbering: even X = stable, odd X = development.4950**"Perl 7" does not exist and is not going to exist as announced.** The history is necessary because51it still causes confusion: it was announced in June 2020 (Sawyer X) as "Perl 5 with modern defaults";52rejection over process and over backward compatibility stopped it, it led to a governance crisis that53produced `perlgov` and the Perl Steering Council, and the original idea died. **The 5.x series54continues without interruption**; `perlpolicy` as of Aug 2026 does not mention Perl 7. If someone is55planning around "when Perl 7 comes out", the plan is wrong.5657**FORBIDDEN to touch the system Perl.** On RHEL/Fedora and Debian/Ubuntu the interpreter and its58modules are part of the OS: they are used by the package manager, boot tooling, `debconf` and59maintenance scripts. Installing modules with `cpan`/`cpanm` as root over `/usr/lib/perl5`, or60replacing the binary, **breaks the operating system** and leaves a machine that is neither61reproducible nor patchable. Rules:62- Modules the OS needs → **distro package** (`perl-*`), never CPAN over the system tree.63- Your own applications → their own interpreter with **`perlbrew`** or **`plenv`**, or at least an64 isolated module tree with **`local::lib`**. Each application with its own tree; never shared.65- In a container: an image with its Perl and its `cpanfile.snapshot`; no `cpanm` at runtime.6667**Dependencies**:6869| Piece | Choice | Verified (MetaCPAN, Aug 2026) |70|---|---|---|71| Installer | **`cpanm`** (App-cpanminus) | 1.7049 (2026-03-17) |72| Declaration | **`cpanfile`** with bounded ranges | — |73| Lock + reproducibility | **`Carton`** (`cpanfile.snapshot`, `carton install --deployment`) | v1.0.35, **2022-05-07** |74| Isolation | `local::lib` | 2.000029, **2022-04-20** |75| Formatting | **perltidy** | 20260705, **GPL-2.0** licence (not "same as Perl") |76| Lint | **Perl::Critic** | **1.156** (2024-10-23) |77| Tests | **Test2::V0** + `prove` / `yath` | see §8 (declared discrepancy) |78| Coverage | `Devel::Cover` | 1.52 (2026-03-07) |79| Auditing | **`CPAN::Audit`** / `cpan-audit` | 20260622.001 (2026-06-22) |8081`Carton` and `local::lib` have gone years without a release: **they work and they are the de facto82standard, but they are not actively maintained software** — a fact to declare, not to hide. `Carmel`83is the same author's alternative and is even more stalled. Criterion: `cpanfile` +84`cpanfile.snapshot` **committed**, and reproducible installation from the snapshot; if the team can85afford it, freezing the module tree in the container image is more robust than trusting a resolution86from CPAN on every build.8788## 3. The language: line zero and what breaks8990**Line zero, non-negotiable, in every `.pl`, `.pm` and `.t` file:**9192```perl93use strict;94use warnings;95```9697Without `strict` no refactor is possible: a typo silently creates a new global symbol. It is the98difference between "maintainable Perl" and "Perl of legend". Acceptable alternative:99`use Modern::Perl` or `use v5.36;` or above — **`use VERSION` with 5.36+ enables `strict`, `warnings`100and `signatures` automatically** and is the preferred form in new code. Migration warning: Perl 5.40101warned that **repeated `use VERSION` declarations stop being allowed in 5.44**.102103- **Sigils and context are the number one source of bugs.** `$x`, `@x` and `%x` are different104 variables, and the sigil indicates **what you get**, not what the variable is (`$array[0]`,105 `$hash{k}`). Above all: **scalar vs list context**. `my ($x) = f();` takes the first element of the106 list; `my $x = f();` puts the function in scalar context (for an array, its length). An empty list107 gives `undef` in scalar context and **disappears** when interpolated into another list. Rule: every108 function returning collections **returns a reference** (`\@result`), except with a deliberate,109 documented list contract. It is the only way to stop context biting you.110- **References**: nested structures only with references (`$h->{a}[0]{b}`). Beware111 **autovivification**: reading `$h->{a}{b}` creates `$h->{a}`; use `exists` if the creation matters.112- **`my` / `our` / `local`**: `my` is lexical and covers 99%. `our` is an alias to the package: only113 constants and `$VERSION`. **`local` is not local**: it is a temporary dynamic value over a global114 (`local $/;`); necessary for special variables, poison for anything else.115- **Object systems**, explicit criteria:116 - **Native `class` in the core**: it has existed since **5.38** (the "Corinna" project), and **it is117 still experimental as of 5.44** (warnings in the `experimental::class` category; the 5.44118 perldelta adds an entry in `perlexperiment` for "New object system and `class` syntax").119 Operational translation: **it is the future and can already be used in an internal project that120 controls its own interpreter, but not in a CPAN distribution nor in a system that cannot absorb a121 syntax change.**122 - **`Object::Pad`** (0.825, 2026-03) is the laboratory `class` came out of, actively maintained: the123 bridge if you want that syntax today and accept following its changes.124 - **`Moo`** (2.005005, **2023-01**): lightweight, no XS, fast startup; stable but without a release125 in years. **`Moose`** (2.4000, 2025-07): complete (roles, *type constraints*, MOP) with a real126 startup and memory cost — only if you already use it or genuinely need the MOP.127 - **Criterion**: a codebase already using Moose → Moose. A new, controlled codebase → native `class`128 accepting its experimental status, or `Moo` if you want stability today. **Never hand-written129 `bless`** in new code, and never two object systems in the same project.130- **Errors**: `eval { }; if ($@) { }` is correct **in a subtle and fragile way**. Real traps: `$@` can131 be clobbered by the destructor of an object released on leaving the `eval`; `$@` false but with an132 error having occurred if the exception was the string `"0"`; and `eval` without a block133 (`eval EXPR`) is something completely different. **Use `Try::Tiny`** (or `feature 'try'` if your134 interpreter has it and you accept its status) and always check the **return value** of `eval`, not135 just `$@`. Exception objects (your own class or `Throwable`) over strings: an error that is only136 text can neither be classified nor retried.137- **Regular expressions: Perl's strong point and its biggest risk.**138 - Precompile with `qr//` anything used in a loop; use `/x` in any pattern longer than one line (with139 comments) — a dense regex without `/x` is unreviewable code.140 - Names, not numbers: `(?<name>...)` and `$+{name}`. `$1`, `$2`… break when you reorder.141 - **ReDoS is real**: Perl's engine does *backtracking* and a pattern with nested quantifiers142 (`(a+)+`) over user input takes the CPU with it. Over untrusted input: bound the subject's length,143 avoid nested quantifiers and overlapping alternations, prefer negated character classes to `.*`,144 and **never accept a user-supplied pattern**.145 - `/e` (evaluates the replacement as code) over external data is `eval STRING` in disguise: vetoed.146147## 4. Quality: formatting, lint, tests148149- **Formatting**: `perltidy` with a committed `.perltidyrc`; `perltidy -b` locally and **`perltidy` in150 check mode as a gate**. No style debates.151- **Lint**: `Perl::Critic` with `.perlcriticrc` in the repo. Severity criteria:152 - New code or an already cleaned-up module: **severity 3** (`--severity 3`, "harsh") as the gate.153 - Legacy entering the pipeline for the first time: **severity 5** (`gentle`) as the gate, dropping154 one level per quarter. Setting severity 1 on a 100k-line legacy codebase does not produce quality,155 it produces a `## no critic` in every file and the abandonment of the linter.156 - Mandatory policies whatever the severity: `RequireUseStrict`, `RequireUseWarnings`,157 `ProhibitTwoArgOpen`, `ProhibitStringyEval`, `ProhibitBacktickOperators`.158 - `## no critic (PolicyName)` **always with the policy named and a reason**; never bare.159 - Warning: Perl::Critic has not published a release since 2024-10 (1.156) — it is still the tool,160 but its cadence is slow.161- **Tests**:162 - `Test2::V0` for new suites; `Test::More` is acceptable and ubiquitous in legacy, **do not migrate163 it for taste**. Runner: `prove -lr t/` (or `yath`), in parallel (`-j`) only if the tests really164 are independent.165 - Cover the happy path, **edges and errors**: empty inputs, `undef`, encoding (Perl and UTF-8 is a166 minefield: decide where you decode and test with non-ASCII), DB errors, timeouts.167 - `Devel::Cover` to measure, **not** as a religious threshold. In legacy with no tests, useful168 coverage is built from the edge: first a characterisation test that pins the current observable169 behaviour (however absurd), then you touch it.170 - Every bugfix leaves a regression test. An unstable test: fix it or delete it.171- **CI gates** (they block the merge, from cheap to expensive):172 1. `perl -c` over every modified file (it compiles).173 2. `perltidy` in check mode.174 3. `Perl::Critic` at the agreed severity.175 4. Full `prove`.176 5. `cpan-audit` over the `cpanfile.snapshot`.177- The suite runs against **the same interpreter version as production**, installed with perlbrew/plenv178 or in the container. "It passes on my machine with the system Perl" is not a signal.179180## 5. Security181182- **Taint mode (`-T`)**: enables the marking of external data (arguments, environment, files, network)183 and makes its use in dangerous operations fatal; it is enabled **automatically** in setuid scripts.184 It is still documented in `perlsec` as a live feature and **is still Perl's only systemic safety185 net** for code processing external input with privileges. Criterion: **enable it in any network,186 CGI or privileged script**; in large legacy, enable it on new entry points. Two limits you must be187 aware of: (a) *untainting* is done with a regex capture, i.e. **your pattern provides the security**,188 not Perl — a `=~ /(.*)/s` is untainting without validation and is worth nothing; (b) there is a189 long-running plan to make taint support a compile-time option and perhaps retire it (see the gap in190 §8): **do not design an architecture whose only defence is taint**.191- **Command execution — the list form versus the string form.** `system("cmd $x")`, `exec("cmd $x")`192 and backticks `` `cmd $x` `` pass the string **through the shell**: direct injection. **Always use193 the list form**: `system('cmd', $x)` / `open(my $fh, '-|', 'cmd', $x)`, which does an `exec` without194 a shell. If you need to capture output with variable arguments, `IPC::Run3`/`IPC::Run` with an195 argument list. **Always** check the exit status (`$?`): an unchecked `system` is a silent failure.196- **Two-argument `open`: FORBIDDEN.** `open(FH, $file)` interprets metacharacters in `$file`: a name197 starting with `>` writes, and one ending in `|` **runs a command**. Always three arguments with an198 explicit mode and a *lexical filehandle*: `open(my $fh, '<', $file) or die ...`. And always check the199 return of `open`, `close`, `print` and `unlink`.200- **`eval STRING`: FORBIDDEN** with any data coming from outside. `eval { BLOCK }` (exception handling)201 is a different construct and is legitimate. Equally vetoed: `/e` in substitutions over external data,202 and `sprintf`/`printf` with a format that comes from the user.203- **Deserialisation**: **`Storable` is unsafe over untrusted input** — `thaw`/`retrieve` over data you204 did not generate yourself is code execution and memory corruption; its own documentation warns about205 it. Vetoed as an interchange format with third parties; acceptable only for data your own process206 wrote into a trusted store. Alternatives: **JSON** (`JSON::PP`/`Cpanel::JSON::XS`, without *blessed207 objects*, with `max_depth`/`max_size`), and **YAML only with a safe loader** (`YAML::PP` in safe208 mode / `YAML::XS` with `$YAML::XS::LoadBlessed = 0`) — a YAML loader that instantiates objects is a209 remote `eval`.210- **SQL**: only DBI *placeholders* (`$dbh->prepare("... WHERE id = ?")` + `execute($id)`).211 Interpolating a variable into SQL is an absolute veto; identifiers that do not accept a placeholder212 (table/column names) are validated against an **allowlist**, never quoted by hand.213- **Paths and files**: canonicalise (`Cwd::realpath`) and check the path is still inside the allowed214 directory before opening; temporaries with `File::Temp`, never predictable names.215- **Secrets** outside the code, the `cpanfile` and the logs; `DBI` with credentials from the216 environment; a `Dumper` of a connection structure carries the password.217- **SCA**: `cpan-audit` over the `cpanfile.snapshot` as a CI gate. CPAN has abandoned modules with no218 successor: every dependency with no release in >5 years that touches network, crypto or parsing gets219 reviewed.220- **Cryptography**: nothing home-made. Password hashing with `Crypt::Argon2` or `Crypt::Bcrypt`;221 **never** `crypt()`, MD5 or SHA-1 for passwords. TLS with `IO::Socket::SSL` **with certificate222 verification enabled** (`SSL_verify_mode => SSL_VERIFY_PEER`) — disabling it is a veto.223224## 6. Performance and operability225226- Before optimising, profile (`Devel::NYTProf`). In Perl the cost is usually in I/O, in badly written227 regexes, in `DBI` doing N+1 and in loading half of CPAN at startup (`Moose` versus `Moo` matters in228 short-lived processes such as CGI or cron).229- Long-lived processes (PSGI/Plack, daemons): **leaks from circular references** — the reference230 counter does not release them; break the cycle with `Scalar::Util::weaken`. Watch RSS: monotonic231 growth = a cycle or an unbounded cache.232- Web deployment: **PSGI/Plack** with Starman/uWSGI behind a proxy. CGI is kept if it already exists;233 it is not chosen.234- Explicit timeouts in every HTTP client (`LWP::UserAgent`, `HTTP::Tiny`) and in the DB; retries with235 backoff only on idempotent operations.236- Structured logs (`Log::Any`/`Log::Log4perl` with JSON output) and a correlation id; `warn`/`die`237 reach the log with context. Scattered `print STDERR` is not observability. In a supervised process,238 autoflush enabled (or you lose the last logs when it dies) and `SIGTERM` handled for a clean239 shutdown.240- Encoding: decide the boundary (`binmode`, `:encoding(UTF-8)`, `use open`) once and document it;241 *mojibake* in Perl is almost always an undeclared boundary.242243## 7. Sustainability: maintain, contain, and when to rewrite244245- **Cadence**: follow Perl's stable series with at most one major version jump per year, and never246 leave the interpreter outside security support (a 3-year window). Upgrading the Perl of a legacy247 application is a project with its own test plan, not a `perlbrew install`.248- Dependencies: quarterly review with `cpan-audit`; an abandoned dependency that touches the attack249 surface is replaced or vendored, and its maintenance is taken on in writing.250- **Document the system while you touch it**: in legacy with no tests, a README with "what it does,251 who calls it, what breaks if it dies" is worth more than a refactor.252253**When it gets rewritten and when it does not** (honest criteria):254- **You do not rewrite** a Perl that works, is in production and **has no tests**. Rewriting without255 tests is exchanging a **known, bounded risk** (ugly code that has worked for 15 years, with all its256 edge cases already discovered by reality) for an **unknown risk** (pretty code that has not found257 any of them yet). The cost is not the one estimated: the expensive part is the invisible list of258 behaviours nobody documented and that somebody depends on continuing to happen.259- **You contain it first**: pin the interpreter, put the repo in CI, add `strict`/`warnings` file by260 file, write characterisation tests over the real inputs and outputs, isolate the module behind a261 stable interface. That work has value even if you never rewrite — and **it is a prerequisite** if262 you do rewrite, because the characterisation tests are the acceptance criteria for the rewrite.263- **You rewrite** when at least one hard condition holds, not an aesthetic one: nobody on the team can264 maintain it and you cannot hire for it; it depends on a Perl version or on modules already out of265 security support with no upgrade path; the business change requires touching the core every sprint;266 or the cost of incidents already exceeds that of the rewrite, **measured**.267- **How you rewrite**: in pieces, with the old and the new coexisting behind an interface (strangler),268 with the characterisation tests as the oracle, and with a declared destination — usually Python (see269 `python-standards`). *Big bang* never.270271**List of prohibitions (veto):**272- ❌ Any file without `use strict; use warnings;` (or `use v5.36;`+). Not even one-line scripts.273- ❌ **FORBIDDEN** to install CPAN modules over the system Perl or replace its binary on RHEL/Debian.274- ❌ **FORBIDDEN**: two-argument `open`.275- ❌ **FORBIDDEN**: `eval STRING` over external data; `/e` with input; `sprintf` with a user-supplied276 format.277- ❌ **FORBIDDEN**: `system`/`exec`/backticks in string form with variable data. Always the list form.278- ❌ **FORBIDDEN**: `Storable::thaw`/`retrieve` over untrusted input; YAML with a loader that279 instantiates objects.280- ❌ SQL by interpolation; `IO::Socket::SSL` without certificate verification; `crypt`/MD5/SHA-1 for281 passwords.282- ❌ User-supplied regex patterns; nested quantifiers over unbounded input.283- ❌ `## no critic` without a named policy and a reason; `$@` as the only failure signal from an `eval`.284- ❌ Hand-written `bless`, or two object systems in the same project.285- ❌ Returning lists from functions that return collections (return references) without a documented286 contract.287- ❌ Deploying without `cpanfile.snapshot`, or installing from CPAN at service startup.288- ❌ Running the suite against a Perl version different from production's.289- ❌ Rewriting a legacy codebase without prior characterisation tests.290- ❌ **Choosing Perl for new code.** With one exception: the team already maintains Perl, already has291 the interpreter, the experience and the internal libraries, and the new work is a natural extension292 of that system. Outside that, hiring, the ecosystem and the tooling cadence play against you: the293 default destination is Python.294295## 8. Mandatory web verification296297Before pinning versions or APIs, **verify online** (WebSearch/WebFetch; MetaCPAN298`https://fastapi.metacpan.org/v1/release/<Dist>` for CPAN versions and licences — **for Perl modules299the source of truth is CPAN, not the GitHub releases**, which are often out of date or non-existent):3001. `perldoc.perl.org/perlpolicy`: which series are supported today (as of Aug 2026: **5.44 and 5.42301 full support, 5.40 security only**) and whether 5.46 is already out. Never deploy an odd X series.3022. The `perldelta` of the target version before any series jump: 5.44 made fatal things that used to303 warn (`goto` into a block, "Attempt to call undefined ... method") and removed repeated304 `use VERSION` declarations.3053. **Status of `use feature 'class'`**: as of Aug 2026 it is **still experimental** (the306 `experimental::class` warning category); check the core tracking issue and the `perlexperiment` of307 your version before betting on it for anything publishable.3084. Versions and licences on CPAN of: Perl::Critic (1.156, no release since 2024-10), perltidy309 (**GPL-2.0**), Carton (**no release since 2022**), local::lib (**2022**), Moo (**2023-01**),310 Moose (2.4000), Object::Pad (0.825), Try::Tiny (MIT), Devel::Cover, CPAN::Audit.3115. Vulnerabilities: an up-to-date `cpan-audit` + osv.dev/GitHub Advisories for the modules with XS.3126. If the project is web: version and status of Mojolicious (9.48 as of Aug 2026, Artistic-2.0),313 Dancer2, DBI and its DBD.314315**Gaps not verified as of Aug 2026** (do not fill from memory; check before deciding):316- **The real status of *taint mode*.** It is documented as alive in `perlsec` and there is a public,317 long-running plan to make it a compile-time option (`-Utaint_support`, introduced around 5.36) and318 eventually retire it. **It is not verified whether the default changed in 5.42/5.44 nor whether319 there is a retirement date**: check it in the `perldelta` of your version and in the core issue320 tracker before basing a design on `-T`.321322**Declared discrepancies**:323- The GitHub releases feed for `Perl-Critic/Perl-Critic` shows **v1.154 (2024-10-21)** while MetaCPAN324 gives **1.156 (2024-10-23)** for the same distribution. MetaCPAN wins (CPAN is the publication325 channel); the GitHub feed is incomplete.326- The MetaCPAN API resolves the module **`Test2::V0` to the `Test-Simple` distribution 1.302222**327 (2026-06-15), not to `Test2-Suite`, and the direct query for the `Test2-Suite` release returned no328 data. **It is not verified** which distribution `Test2::V0` lives in today: check it on MetaCPAN329 before writing it into a `cpanfile`.330331If the web contradicts this document, **the web wins** — flag the discrepancy.