perl.gg / hidden-gems

<!-- category: hidden-gems -->

Devel::Peek - X-Ray Vision for Perl Variables

2026-04-22

The number 42 is a number. Obviously. But is it a number inside Perl? Or is it a string that looks like a number? Or is it both at the same time?
use Devel::Peek; my $x = 42; Dump($x);
SV = IV(0x7fa3b2c04e90) at 0x7fa3b2c04ea0 REFCNT = 1 FLAGS = (IOK,pIOK) IV = 42
That is the internal representation. An integer scalar (IV) with the value 42. The flags say IOK: "integer value is OK to use." Perl knows this is a number and nothing else.

Now do something with it as a string:

my $x = 42; my $s = "$x"; # force string context Dump($x);
SV = PVIV(0x7fa3b2c14280) at 0x7fa3b2c04ea0 REFCNT = 1 FLAGS = (IOK,POK,pIOK,pPOK) IV = 42 PV = 0x7fa3b2d05230 "42"\0 CUR = 2 LEN = 10
Now it has BOTH an integer value (IV = 42) AND a string value (PV = "42"). Two representations cached in the same variable. Perl computed the string version when you interpolated, then kept it around in case you need it again.

This is Devel::Peek. It shows you what Perl actually stores inside a variable, at the C structure level. When something behaves unexpectedly, this is where you go to find out why.

Part 1: THE DUMP FUNCTION

Dump is the main function. Give it a variable, it shows you everything:
use Devel::Peek; my $num = 3.14; Dump($num);
SV = NV(0x7fa3b2c04e88) at 0x7fa3b2c04ea0 REFCNT = 1 FLAGS = (NOK,pNOK) NV = 3.14
NV means "numeric value" (floating point). The flags say NOK: "numeric value is OK."

A string:

my $str = "hello"; Dump($str);
SV = PV(0x7fa3b2c04e78) at 0x7fa3b2c04ea0 REFCNT = 1 FLAGS = (POK,pPOK) PV = 0x7fa3b2d05230 "hello"\0 CUR = 5 LEN = 10
PV means "pointer value" (string). POK means "pointer value is OK." CUR is the current length. LEN is the allocated buffer size.

The \0 at the end of the PV is the null terminator. Perl keeps it there for C compatibility, but it is not part of the string length.

Part 2: READING THE FLAGS

The FLAGS line is the most important part of the dump. It tells you exactly what Perl thinks this variable is:
FLAG MEANING ------ ------------------------------------------ IOK Integer value is valid NOK Numeric (float) value is valid POK String (PV) value is valid pIOK Private: integer value is valid pNOK Private: numeric value is valid pPOK Private: string value is valid ROK Reference value is valid UTF8 String is stored as UTF-8
The "p" versions (pIOK, pNOK, pPOK) are internal flags. For debugging purposes, the non-p versions are what you care about.

When you see multiple flags like (IOK,POK,pIOK,pPOK), it means the variable has both an integer and a string representation cached. Perl will use whichever one the current operation needs without converting.

Part 3: THE DUAL-NATURED SCALAR

This is the big reveal. A Perl scalar can be a string AND a number simultaneously:
use Devel::Peek; my $x = "42"; Dump($x); say "--- after numeric use ---"; my $y = $x + 0; Dump($x);
SV = PV(0x...) at 0x... REFCNT = 1 FLAGS = (POK,pPOK) PV = 0x... "42"\0 CUR = 2 LEN = 10 --- after numeric use --- SV = PVIV(0x...) at 0x... REFCNT = 1 FLAGS = (IOK,POK,pIOK,pPOK) IV = 42 PV = 0x... "42"\0 CUR = 2 LEN = 10
After the first dump, $x is purely a string (POK only). After we use it as a number ($x + 0), Perl converts it to an integer and caches the result. Now it has both IV = 42 and PV = "42".

The next time you use $x as a number, Perl grabs the cached IV directly. No string-to-number conversion needed.

This caching is why Perl is faster than you might expect at repeated numeric operations on "number-like" strings.

Part 4: WHY == AND EQ GIVE DIFFERENT RESULTS

Here is a classic Perl confusion that Devel::Peek explains instantly:
my $a = "00"; my $b = "0"; say $a == $b ? "equal" : "not equal"; # equal (numeric) say $a eq $b ? "equal" : "not equal"; # not equal (string)
What is going on? Look at the internals:
use Devel::Peek; my $a = "00"; Dump($a);
SV = PV(0x...) at 0x... REFCNT = 1 FLAGS = (POK,pPOK) PV = 0x... "00"\0 CUR = 2 LEN = 10
The string "00" is stored as two characters. When == compares, it converts both to numbers: "00" becomes 0, "0" becomes 0, they are equal. When eq compares, it compares the raw strings: "00" vs "0", different lengths, not equal.

Neither answer is wrong. You asked two different questions. The == asked "are these the same number?" The eq asked "are these the same string?" Devel::Peek shows you why: the PV is "00", which is a different string from "0" but the same number as 0.

Part 5: SPOTTING THE UTF-8 FLAG

The UTF8 flag is critical for debugging encoding issues:
use Devel::Peek; my $bytes = "\xc3\xa9"; # raw bytes my $char = "\x{e9}"; # Unicode character Dump($bytes); say "---"; Dump($char);
SV = PV(0x...) at 0x... REFCNT = 1 FLAGS = (POK,pPOK) PV = 0x... "\303\251"\0 CUR = 2 LEN = 10 --- SV = PV(0x...) at 0x... REFCNT = 1 FLAGS = (POK,pPOK,UTF8) PV = 0x... "\303\251"\0 [UTF8 "\x{e9}"] CUR = 2 LEN = 10
Both have the same bytes in the PV. But the second one has the UTF8 flag set, and the dump shows the decoded character in brackets: [UTF8 "\x{e9}"].

With the UTF8 flag, length($char) returns 1 (one character). Without it, length($bytes) returns 2 (two bytes). Same bytes, different interpretation. The flag is the switch.

When you get garbled text or "Wide character" warnings, dump the variable. If the UTF8 flag is missing when it should be there (or present when it should not be), that is your bug.

Part 6: REFERENCE COUNTING

Every Perl variable has a reference count (REFCNT). It tracks how many things point to this value:
use Devel::Peek; my $x = "hello"; Dump($x); # REFCNT = 1 my $ref = \$x; Dump($x); # REFCNT = 2 { my $ref2 = \$x; Dump($x); # REFCNT = 3 } Dump($x); # REFCNT = 2 (ref2 went out of scope)
When REFCNT hits 0, Perl frees the memory. This is Perl's garbage collection: simple reference counting.

Circular references break this:

my $a = {}; my $b = {}; $a->{other} = $b; $b->{other} = $a; # REFCNT of both is 2 # when $a and $b go out of scope, REFCNT drops to 1, not 0 # memory leak!
Dump the structures and you can see the REFCNT never reaching zero. This is why Scalar::Util::weaken exists.

You can also get just the reference count without a full dump:

use Devel::Peek qw(SvREFCNT); my $x = "hello"; my $ref = \$x; say SvREFCNT($x); # 2

Part 7: ARRAYS AND HASHES

Dump works on arrays and hashes too:
use Devel::Peek; my @arr = (1, "two", 3.0); Dump(\@arr);
SV = IV(0x...) at 0x... REFCNT = 1 FLAGS = (TEMP,ROK) RV = 0x... SV = PVAV(0x...) at 0x... REFCNT = 1 FLAGS = () ARRAY = 0x... FILL = 2 MAX = 3 FLAGS = (REAL) Elt No. 0 SV = IV(0x...) at 0x... REFCNT = 1 FLAGS = (IOK,pIOK) IV = 1 Elt No. 1 SV = PV(0x...) at 0x... REFCNT = 1 FLAGS = (POK,pPOK) PV = 0x... "two"\0 CUR = 3 LEN = 10 Elt No. 2 SV = NV(0x...) at 0x... REFCNT = 1 FLAGS = (NOK,pNOK) NV = 3
FILL is the highest used index (2, meaning 3 elements). MAX is the allocated size (Perl over-allocates for growth). Each element is a separate SV with its own type and flags.

For hashes, you can see the bucket structure:

my %h = (name => "Larry", age => 69); Dump(\%h);
This shows the hash internals, including key-value pairs and the underlying hash table structure.

Part 8: PRACTICAL DEBUGGING

Problem: string comparison fails mysteriously.
my $a = get_from_database(); # returns "hello" my $b = get_from_user_input(); # returns "hello" if ($a eq $b) { say "match"; } else { say "no match"; # this fires. why? }
Dump both:
Dump($a); Dump($b);
SV = PV(0x...) at 0x... FLAGS = (POK,pPOK,UTF8) PV = 0x... "hello"\0 [UTF8 "hello"] SV = PV(0x...) at 0x... FLAGS = (POK,pPOK) PV = 0x... "hello"\0
One has the UTF8 flag, the other does not. Even though the bytes are identical, Perl compares them differently when the flags disagree. Fix it by normalizing both to the same encoding:
use Encode qw(decode encode); $a = decode('UTF-8', encode('UTF-8', $a)); $b = decode('UTF-8', encode('UTF-8', $b));
Or use utf8::upgrade on both.

Problem: number that is not a number.

my $val = "42\n"; # trailing newline from chomp failure say $val + 1; # 43, but with a warning Dump($val);
SV = PV(0x...) at 0x... FLAGS = (POK,pPOK) PV = 0x... "42\n"\0 CUR = 3
The PV shows "42\n". There it is. A hidden newline. The chomp was missed or failed. Devel::Peek makes invisible characters visible because it shows escape sequences in the PV dump.

Part 9: DUMPING COMPLEX STRUCTURES

For deep data structures, Dump gets verbose. You can control the depth:
use Devel::Peek; my $data = { users => [ { name => "Alice", age => 30 }, { name => "Bob", age => 25 }, ], }; Dump($data, 2); # only go 2 levels deep
The second argument limits recursion depth. Without it, Dump follows every reference to the bottom, which can produce thousands of lines for complex structures.

For quick overviews, sometimes Data::Dumper is more readable:

use Data::Dumper; say Dumper($data); # human-friendly structure dump
But Data::Dumper shows you the Perl-level view. Devel::Peek shows you the C-level view. When Data::Dumper says two things are the same but they behave differently, Devel::Peek tells you why.

They complement each other. Use Data::Dumper first to see the structure. Use Devel::Peek when something in the structure does not behave as expected.

Part 10: THE CONVERSION CACHE

The most important insight from Devel::Peek is understanding Perl's conversion caching.
use Devel::Peek; my $x = "123.45"; say "Step 1: string only"; Dump($x); my $y = $x + 0; say "Step 2: after numeric use"; Dump($x); my $z = "$x"; say "Step 3: after string use (already cached)"; Dump($x);
Step 1: string only FLAGS = (POK,pPOK) PV = "123.45" Step 2: after numeric use FLAGS = (NOK,POK,pNOK,pPOK) NV = 123.45 PV = "123.45" Step 3: after string use (already cached) FLAGS = (NOK,POK,pNOK,pPOK) NV = 123.45 PV = "123.45"
Step 1: just a string. Step 2: Perl computed the numeric value and cached it (NV appears alongside PV). Step 3: both representations already existed, no new conversion needed.

This is why Perl does not re-parse "123.45" every time you use it as a number. The first conversion is cached. Every subsequent use is a direct read from the cached slot.

It also explains a subtle behavior. Once a variable has been used as both a string and a number, it keeps both representations even if you change one:

my $x = "42"; $x + 0; # cache the IV $x = "99"; # assign a new string value Dump($x); # FLAGS = (POK,pPOK) - IV cache is invalidated # PV = "99" # The old IV = 42 is gone
Assignment clears the stale cache. Perl is not going to serve you yesterday's value from the numeric slot when you changed the string today.
Perl's internal variable structure: +---------------------------+ | SV (Scalar Value) | +---------------------------+ | REFCNT: 1 | | FLAGS: IOK, POK, UTF8 | +---------------------------+ | IV: 42 (integer) | | NV: 42.0 (float) | | PV: "42\0" (string) | | CUR: 2 (strlen) | | LEN: 10 (bufsize) | +---------------------------+ .--. |o_o | "Same variable. |:_/ | Three values. // \ \ Zero contradictions." (| | ) /'\_ _/`\ \___)=(___/
Devel::Peek is not a tool you use every day. It is a tool you use when nothing else explains the behavior. When Data::Dumper says two variables are identical but eq says they are not. When a number works in one context but not another. When UTF-8 is sometimes right and sometimes wrong and you cannot figure out why.

Those are Devel::Peek moments. You dump the variable, you see the flags, you see the cached values, and the mystery evaporates.

It is Perl's MRI machine. You do not use it for a checkup. You use it for a diagnosis.

perl.gg