Skip to content
Blog

Digit Separator Underscores in Dart 3.6

Dart 3.6 added underscore digit separators for readable number literals. Here's what changed and how to use the syntax.

Published on • October 20, 2026

AI Assistant

Counting zeros in a number literal is a small, recurring tax. Is that eight zeros or nine? A mistyped digit in a byte count or a timeout constant compiles happily and fails at runtime — or worse, silently computes the wrong value. Dart 3.6, released December 11, 2024, fixed this with a one-character solution: underscores as digit separators inside number literals.

The change

Per the Dart language evolution history, Dart 3.6 “added support for digit separator underscores (_) to the language. Digit separators improve readability of long number literals.” The official example:

var m = 1__000_000__000_000__000_000;

The underscores are ignored by the compiler — the literal’s value is identical to the version without them. They exist purely for you and whoever reads your code next.

Before and after

The practical value shows up in the constants that litter real apps:

// Before Dart 3.6:
const cacheTimeout = 300000;        // 5 minutes? 30000? depends how you squint
const maxUploadSize = 10485760;     // 10 MB in bytes, if you can parse it
const sessionExpiry = 1800000;      // half an hour, allegedly

// With digit separators:
const cacheTimeout = 300_000;
const maxUploadSize = 10 * 1024 * 1024;
const sessionExpiry = 1_800_000;

10485760 and 10_485_760 are the same number, but only one of them can be verified at a glance. This matters most in code review, where you’re validating someone else’s magic numbers against a spec or a bug report.

Where separators earn their keep

  • Byte sizes and transfer limits: 5_242_880 instead of a digit salad when the limit is 5 MB.
  • Timeouts in milliseconds: 30_000 reads instantly as thirty seconds; 30000 requires a pause.
  • Large IDs and money in minor units: order totals stored as integers get long fast — 129_900 cents is self-documenting in a way 129900 never will be.
  • Mask and bit-pattern-adjacent literals: grouping makes it visually obvious how many digits are actually present, catching off-by-one typos before they ship.

One subtlety visible in the official example: consecutive underscores (1__000...) are allowed. The separators are lexical noise the compiler strips, not a strict every-third-digit convention — the grouping style is up to you, though standardizing on thousands grouping is what most style guides recommend.

Zero migration cost

A nice property of this feature: because separators are purely lexical, adopting them is a text-only change. No imports, no SDK runtime requirement beyond the language version gate, and no behavioral risk — the compiled output for 300_000 and 300000 is identical. That also means the change is trivially reviewable: a diff that only inserts underscores into literals can be scanned in seconds, and tools like dart format won’t fight you over the grouping.

It also means the opposite direction is safe. If you’re maintaining a fork or backporting a patch to a branch still pinned below Dart 3.6, the fix is exactly mechanical: strip the underscores before applying the literal to the older branch. Nothing else about the code moves.

A note on consistency

The separators are permissive — the compiler doesn’t enforce grouping — so a codebase that adopts them benefits from an explicit convention. Teams commonly agree on one of two styles: thousands grouping (1_000_000) for general magnitudes, or semantic grouping that mirrors how the quantity is defined (1024 * 1024-style byte math written as 1_048_576). What matters less than which convention you pick is that reviewers treat an incorrectly grouped literal as a bug, because the whole value of the feature is that a miscounted digit becomes visible. If 10000000 was supposed to be ten million and someone writes 1_000_000_0, the underscore pattern makes the mistake pop out in a way the unseparated version never could.

Availability

Like all post-2.0 language features, digit separators are gated by your package’s language version. Set an SDK lower constraint of at least 3.6.0 in pubspec.yaml:

environment:
  sdk: '>=3.6.0 <4.0.0'

Your package’s language version derives from that lower bound, so a package still pinned to >=3.0.0 <4.0.0 won’t get the feature no matter which SDK compiles it — that’s the mechanism that keeps formatting and syntax stable across a team.

A small feature, a real payoff

Digit separators don’t change what your code computes; they change how quickly a human can verify that it computes the right thing. In a language that already leans on 1024 * 1024-style expressions for readability, underscores close the gap for cases where a single literal is genuinely the clearest option. If you’re on Dart 3.6 or later, reach for them whenever a literal grows past four or five digits.

Incidentally, the evolution page itself is worth a bookmark even though it’s now deprecated in favor of the changelog and the language versioning docs — it remains the fastest way to answer “which Dart release did this feature land in?”

Sources