Perl Subroutine Signatures: Ditch the @_ Boilerplate Without Breaking Production
Perl's subroutine signatures are stable since 5.36. Here's what they replace, how arity checking catches silent bugs, and when @_ unpacking is still the right call.
23 Dec 2025, 09:20 UTC

Every Perl developer knows the ritual. You write a subroutine, and before you can do anything useful, you type my ($name, $greeting) = @_; — the same unpacking boilerplate you've typed ten thousand times. Worse, if a caller passes the wrong number of arguments, this line says nothing. It silently assigns undef and lets the bug surface somewhere far away.
Subroutine signatures fix both problems, and as of Perl 5.36 (released 2022) they're a stable language feature, not an experiment. If your production floor is 5.36 or newer, there's little reason to keep writing manual unpacking.
What signatures actually give you
With signatures, you declare parameters directly in the subroutine declaration:
use v5.36;
sub greet ($name, $greeting = 'Hello') {
return "$greeting, $name!";
}
say greet('Ada'); # Hello, Ada!
say greet('Ada', 'Hi'); # Hi, Ada!
Two things happen here that @_ unpacking never gave you for free:
- Arity checking. Call
greet()with no arguments or with three, and Perl raises a runtime error (Too few arguments/Too many arguments) at the call site. Manual unpacking silently tolerates both. - Defaults and optional parameters.
$greeting = 'Hello'applies when the caller omits the argument or passesundef.
Slurpy parameters cover the rest of the common calling conventions: sub log_all ($level, @messages) collects remaining scalars into an array, and sub configure (%opts) collects key/value pairs into a hash.
A worked example: refactoring a real helper
Here's a typical utility sub written in the classic style:
sub format_price {
my ($amount, $currency) = @_;
$currency //= 'USD';
return sprintf '%s %.2f', $currency, $amount;
}
The signature version:
use v5.36;
sub format_price ($amount, $currency = 'USD') {
return sprintf '%s %.2f', $currency, $amount;
}
Same behavior, less code — and now format_price(19.99, 'EUR', 'extra') fails loudly instead of being silently ignored. Run this with any Perl 5.36+ interpreter to check:
perl -v # confirm you're on 5.36 or newer
perl -Mv5.36 -e 'sub add ($a, $b) { $a + $b } say add(2, 3);' # prints 5
perl -Mv5.36 -e 'sub add ($a, $b) { $a + $b } add(1);' # dies: Too few arguments
No special permissions are needed; these are read-only one-liners. The second command should print 5; the third should fail with an arity error. If the first command reports 5.34 or older, read the next section before adopting signatures.
Version history matters for production
Signatures appeared in Perl 5.20 as an experimental feature, and details of the syntax shifted slightly across releases. On 5.20 through 5.34 you must opt in and silence the warning:
use feature 'signatures';
no warnings 'experimental::signatures';
That's workable for internal code, but shipping experimental-feature code in a library means your behavior depends on which 5.2x/5.3x release your users run. The clean line is 5.36: signatures are stable, enabled by use v5.36, and need no warning suppression. Check perldoc perlsub on your installed version to confirm the feature status before committing.
One more compatibility note: signatures are a compile-time feature, and code using them won't even parse on Perl older than 5.20. If you maintain a CPAN module or in-house library that must run on the ancient system perls still found on long-lived enterprise boxes, the @_ style remains the pragmatic choice there.
Trade-offs and gotchas
- No named arguments. Signatures are positional. The usual workaround is a slurpy hash —
sub new ($class, %args)— but that gives up arity checking on the hash contents, so you're back to validating keys yourself. - Methods still need
$self. A method's invocant is just the first argument: writesub rename ($self, $new_name)explicitly. This is actually clearer than the magicshift, but it surprises people the first time. - Team and tooling consistency. Mixed codebases where half the subs use signatures and half unpack
@_are harder to read than either pure style. Adopt per-project, not per-file.
Performance, at least, is a non-issue: signatures compile to efficient interpreter code with no meaningful overhead versus manual assignment.
Where to start
Pick one module in a codebase that already requires 5.36, convert its subroutines to signatures, and run the test suite. The arity errors will either stay quiet — confirming your callers are correct — or immediately surface latent bugs where someone was passing arguments that were being ignored. Either outcome is a win, and the diff will almost certainly delete more lines than it adds.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.