| |

Dart 21 🎯 Member Visibility: Private Identifiers (_) and Library Scope Rules

Every language has a way of saying “this is not for you.” In Java, it is the private keyword. In Python, it is a naming convention with underscores. In Dart, it is a single leading underscore. There is no private keyword, no protected, no public. An identifier that starts with _ is private to the library it lives in. Everything else is public. This is not a convention that linters enforce—it is a compiler rule. A private member is inaccessible outside its library, and the analyzer will reject any attempt to reach it.

This chapter covers Dart’s visibility model in full. You will learn what a library is, why privacy is scoped to the library rather than the class, how the underscore interacts with classes, top-level declarations, and files, and how the part and part of directives change the scope of privacy. You will also see the patterns that Dart developers use to work with the underscore effectively and the mistakes that arise when the model is misunderstood.

Key point: Privacy in Dart is library-level, not class-level. A leading underscore makes an identifier private to the library in which it is declared. Two classes in the same file can access each other’s private members because they share a library. A class in another file cannot, even if it is in the same package.


Why Dart’s visibility model works this way

The class-scope problem. In languages like Java and C#, privacy is scoped to the class. A private field is accessible only within the class that declares it. But this means that two closely related classes—a class and its helper, a class and its builder, a class and its test double—cannot share private state without exposing it publicly or using workarounds like package-private access or reflection. Dart’s library-scoped privacy allows related classes in the same file to cooperate fully while still hiding implementation from the rest of the program.

The convention problem. Python has no compiler-enforced privacy. Underscores are a strong convention that tools and developers respect, but nothing stops external code from reaching in. Dart makes the underscore a hard rule. The analyzer treats private identifiers as inaccessible, and the compiler enforces the restriction. This means developers can rely on the underscore as a real boundary, not just a hint.

The library problem. A Dart library is not the same as a file, though in most projects the two coincide. A library is the unit of privacy. A file with no part directives is a single library. A file that declares part of a library shares that library’s privacy scope. A library file that uses part to include other files extends its scope to include them. Understanding this distinction is essential for reasoning about privacy across files.

The testing problem. Dart’s privacy model is sometimes criticized because tests cannot access private members of a class in a different file. The standard solution is to place tests in the same library using part, or to expose test-only accessors behind a naming convention. But this is a trade-off, not a flaw. The boundary is real, and working around it requires deliberate choices.

The encapsulation problem. Good design hides implementation details and exposes a minimal public API. Dart’s library-scoped privacy encourages this by making it easy to keep helpers private. A file can expose one public class and several private helpers, and the helpers are invisible to consumers of the class. This pattern is common in Dart packages and is supported directly by the language.


a. Private identifiers and library scope

An identifier that begins with an underscore is private to its library. This applies to top-level declarations, class members, and local variables.

// top-level private variable
int _counter = 0;

// top-level private function
void _increment() {
  _counter++;
}

// public class with private members
class Counter {
  int _value = 0;

  void increment() {
    _value++;
  }

  int get value => _value;
}

The top-level _counter and _increment are accessible from anywhere in the same file. They are not accessible from other files, even if those files import this one. The _value field is private to the library, so any code in the same file can read it, including other classes, but code in other files cannot.

A private member is not truly “invisible.” It exists at runtime, and the identifier is present in the compiled output. Dart’s privacy is a compile-time restriction, not an obfuscation. The analyzer enforces it during development, and the compiler enforces it during compilation. Attempting to access a private member from another library produces an error at compile time, not a warning.

// other_file.dart
import 'counter.dart';

void main() {
  final c = Counter();
  c.increment();     // OK
  print(c.value);    // OK
  print(c._value);   // Error: '_value' is private
}

The analyzer message is explicit: _value is defined in a different library and cannot be accessed. There is no way to bypass this except by editing the library itself.


b. Privacy in classes and top-level declarations

Within a class, private members are accessible to all code in the same library. This includes other classes, mixins, extensions, and top-level functions.

class Person {
  String _name;

  Person(this._name);

  String get name => _name;
}

class Greeter {
  String greet(Person person) {
    return 'Hello, ${person._name}';  // OK: same library
  }
}

The Greeter class can read Person._name because both classes are in the same library. If they were in separate files with no part directive, the access would fail.

The same applies to constructors. A private named constructor is accessible only within the library.

class Token {
  final String value;

  Token._internal(this.value);

  factory Token.generate() => Token._internal('generated');
}

The _internal constructor is private. External code cannot call Token._internal('x'). The public factory Token.generate can call it because it is in the same library.

Private static members work the same way. A private static field or method is accessible to the class and to other code in the same library.

class Config {
  static final _cache = <String, String>{};

  static String get(String key) => _cache[key] ?? '';
  static void set(String key, String value) => _cache[key] = value;
}

The _cache map is private to the library. External code uses Config.get and Config.set to interact with it.


c. The part and part of directives

The part and part of directives extend a library across multiple files. A file declared as part of a library shares that library’s privacy scope. Private members declared in the library file are accessible in the part file, and vice versa.

// counter.dart
library counter;

part 'counter_helpers.dart';

int _counter = 0;
// counter_helpers.dart
part of 'counter.dart';

void incrementCounter() {
  _counter++;  // OK: same library
}

The part of directive can use either the library name or the library’s file path. The older syntax used the library name; the modern syntax uses the file path.

part of 'counter.dart';

A file can only be a part of one library. A library can have multiple parts. The privacy scope is the union of the library file and all its part files.

The part directive is less common in modern Dart than it was in earlier versions. Most packages now use multiple libraries with public APIs and keep private members within a single file. But the mechanism still exists, and it is the only way to share private members across files without exposing them publicly.

The library directive itself is optional in Dart 2 and later. A file becomes a library simply by existing. The library directive is used only when you want to attach metadata like documentation or annotations.

/// A library for counter operations.
library;

part 'counter_helpers.dart';

The library; with no name is the modern form. The older form library counter; still works but is less common.


Complete Example Session

// ============================================
// PART 1: PRIVATE TOP-LEVEL DECLARATIONS
// ============================================
int _counter = 0;

void _increment() {
  _counter++;
}

int getCounter() => _counter;
// ============================================
// PART 2: PRIVATE CLASS MEMBERS
// ============================================
class Counter {
  int _value = 0;

  void increment() {
    _value++;
  }

  int get value => _value;
}
// ============================================
// PART 3: ACCESSING PRIVATE MEMBERS IN SAME LIBRARY
// ============================================
class CounterReader {
  int read(Counter counter) => counter._value;  // OK: same library
}
// ============================================
// PART 4: PRIVATE CONSTRUCTOR
// ============================================
class Token {
  final String value;

  Token._internal(this.value);

  factory Token.generate() => Token._internal('generated');
}
// ============================================
// PART 5: PRIVATE STATIC MEMBERS
// ============================================
class Config {
  static final _cache = <String, String>{};

  static String get(String key) => _cache[key] ?? '';
  static void set(String key, String value) => _cache[key] = value;
}
// ============================================
// PART 6: PRIVATE GETTERS AND SETTERS
// ============================================
class Account {
  double _balance = 0;

  double get balance => _balance;
  set _balanceDirect(double value) => _balance = value;
}
// _balanceDirect setter is private
// ============================================
// PART 7: LIBRARY WITH PART
// ============================================
// math_lib.dart
library;

part 'math_helpers.dart';

int _result = 0;
// ============================================
// PART 8: PART FILE SHARING PRIVACY
// ============================================
// math_helpers.dart
part of 'math_lib.dart';

void add(int a, int b) {
  _result = a + b;  // OK: same library
}
// ============================================
// PART 9: PRIVATE MEMBERS IN MIXINS
// ============================================
mixin _CounterMixin {
  int _count = 0;
  void _incrementCount() => _count++;
}

class Widget with _CounterMixin {
  int get count => _count;
}
// Both declarations are in same library
// ============================================
// PART 10: PRIVATE EXTENSION
// ============================================
extension _StringHelpers on String {
  String get shout => toUpperCase();
}

void demo() {
  print('hello'.shout);  // OK: same library
}
// 'shout' is not available outside this library

The ten parts covered private top-level declarations, private class members, same-library access, private constructors, private static members, private getters and setters, the part directive, the part of directive, private mixins, and private extensions.


Quick Reference

Privacy Rules

DeclarationPrivate SyntaxAccessible From
Top-level variable_nameSame library
Top-level function_name()Same library
Class field_nameSame library
Class method_name()Same library
ConstructorClassName._name()Same library
Static field_nameSame library
Getter/setter_nameSame library
Extension_NameSame library
Mixin_NameSame library

Library Scope

File TypePrivacy Scope
Standalone fileThe file itself
Library file with partLibrary file + all parts
part of fileThe library it belongs to
Multiple files without partEach file is its own library

What Underscore Does Not Do

MisconceptionReality
Makes member private to classPrivate to library
Hides member at runtimeCompile-time only
Works across packagesWorks within a library only
Applies to librariesApplies to identifiers

Common Private Patterns

PatternPurpose
_internal constructorPrevent direct instantiation
_cache static mapInternal storage
_name fieldEncapsulated state
_helper() functionImplementation detail
_Result classInternal data type

Best Practices

✅ Do This:

// Use underscore for implementation details
int _counter = 0;                                              // ✅

// Keep private members in the same library as their users
class Counter {
  int _value = 0;
  int get value => _value;
}                                                              // ✅

// Use private constructors for controlled instantiation
class Token {
  Token._internal(this.value);
  factory Token.generate() => Token._internal('x');
}                                                              // ✅

// Use part files when private sharing is needed
library;
part 'helpers.dart';                                           // ✅

// Make helper functions private
void _log(String message) => print('[LOG] $message');          // ✅

// Private extensions for library-only utilities
extension _StringX on String {
  String get shout => toUpperCase();
}                                                              // ✅

❌ Don’t Do This:

// Don't expect class-level privacy
class A {
  int _x = 0;
}
class B {
  int read(A a) => a._x;  // ⚠️ works only if same library
}                                                              // ⚠️

// Don't try to access private members from other files
// other.dart
import 'counter.dart';
void main() {
  print(Counter()._value);                                     // ❌ error
}

// Don't use underscore as a naming convention without intent
class PublicAPI {
  int _internalButPublic = 0;  // ⚠️ actually private
}                                                              // ⚠️

// Don't split related classes into separate files without part
// if they need to share private members                       // ❌

// Don't forget that part files share the library's privacy
// and can access each other's private members                 // ⚠️

Common Pitfalls

PitfallWhy It HappensFix
Cannot access _member from another fileFile is a different libraryMove both into same library with part
Test cannot reach private memberTest is in separate fileUse part or test through public API
Private member visible in another fileBoth files are parts of same libraryEnsure files are separate libraries
Underscore on a public APINaming mistakeRemove underscore
Private constructor not callableCalled from another libraryUse a public factory
part of path mismatchWrong file pathMatch the library file path exactly
Private extension not foundExtension is in different libraryMove extension or expose via public API

Real-World Examples

1. Private Field with Getter

class User {
  String _name = '';
  String get name => _name;
  set name(String value) => _name = value.trim();
}

2. Private Constructor

class Singleton {
  static final Singleton _instance = Singleton._();
  Singleton._();
  factory Singleton() => _instance;
}

3. Private Static Cache

class UserCache {
  static final _users = <int, User>{};
  static User? get(int id) => _users[id];
}

4. Private Top-Level Helper

String _formatDate(DateTime date) =>
    '${date.year}-${date.month}-${date.day}';

5. Private Extension

extension _ListX<T> on List<T> {
  T? get firstOrNull => isEmpty ? null : first;
}

6. Part File for Helpers

// library.dart
library;
part 'helpers.dart';

7. Private Mixin

mixin _Loggable {
  void _log(String msg) => print('[LOG] $msg');
}

8. Private Named Constructor

class Color {
  final int value;
  Color._(this.value);
  factory Color.rgb(int r, int g, int b) => Color._((r << 16) | (g << 8) | b);
}

9. Private Getter

class Temperature {
  double _celsius = 0;
  double get _fahrenheit => _celsius * 9 / 5 + 32;
}

10. Part of File

// helpers.dart
part of 'library.dart';

void increment() => _counter++;

Visual

Library Privacy Scope

┌─────────────────────────────────────────────────────────────┐
│  LIBRARY SCOPE                                              │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  counter.dart (library)                             │    │
│  │                                                     │    │
│  │  int _counter = 0;        ← private to library      │    │
│  │  void _increment() { ... } ← private to library     │    │
│  │                                                     │    │
│  │  class Counter {                                    │    │
│  │    int _value = 0;        ← private to library      │    │
│  │  }                                                  │    │
│  │                                                     │    │
│  │  ┌───────────────────────────────────────────────┐  │    │
│  │  │ part 'helpers.dart';                          │  │    │
│  │  └───────────────────────────────────────────────┘  │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  helpers.dart (part of counter.dart)                │    │
│  │                                                     │    │
│  │  part of 'counter.dart';                            │    │
│  │                                                     │    │
│  │  void doWork() {                                    │    │
│  │    _counter++;      ← OK: same library              │    │
│  │  }                                                  │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  other.dart (separate library)                      │    │
│  │                                                     │    │
│  │  import 'counter.dart';                             │    │
│  │                                                     │    │
│  │  void main() {                                      │    │
│  │    print(_counter);  ← ERROR: private               │    │
│  │  }                                                  │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Class vs Library Privacy

┌─────────────────────────────────────────────────────────────┐
│  JAVA-STYLE (Class Privacy)                                 │
│                                                             │
│  class A {                                                  │
│    private int x;                                           │
│  }                                                          │
│  class B {                                                  │
│    void f(A a) { a.x = 1; }  ← ERROR: private to A          │
│  }                                                          │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  DART-STYLE (Library Privacy)                               │
│                                                             │
│  class A {                                                  │
│    int _x = 0;                                              │
│  }                                                          │
│  class B {                                                  │
│    void f(A a) { a._x = 1; }  ← OK: same library            │
│  }                                                          │
│                                                             │
│  Privacy is shared across all declarations in the library.  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Part and Part Of

┌─────────────────────────────────────────────────────────────┐
│  WITHOUT PART: TWO SEPARATE LIBRARIES                       │
│                                                             │
│  file_a.dart              file_b.dart                       │
│  ┌──────────────┐         ┌──────────────┐                  │
│  │ int _x = 0;  │         │ import a;    │                  │
│  │              │         │ print(_x);   │ ← ERROR          │
│  └──────────────┘         └──────────────┘                  │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  WITH PART: ONE LIBRARY, TWO FILES                          │
│                                                             │
│  library.dart             part_file.dart                    │
│  ┌──────────────┐         ┌──────────────┐                  │
│  │ library;     │         │ part of      │                  │
│  │ part 'part_  │         │ 'library.dart';│                │
│  │ file.dart';  │         │              │                  │
│  │ int _x = 0;  │         │ void f() {   │                  │
│  │              │         │   _x++;      │ ← OK             │
│  └──────────────┘         └──────────────┘                  │
│                                                             │
│  Both files share one privacy scope.                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Underscore on Different Declarations

┌─────────────────────────────────────────────────────────────┐
│  WHAT UNDERSCORE APPLIES TO                                 │
│                                                             │
│  _variable              ← private top-level variable        │
│  _function()            ← private top-level function        │
│  _ClassName             ← private class                     │
│  _TypeName              ← private type alias                │
│  _enumName              ← private enum                      │
│  _extensionName         ← private extension                 │
│  _mixinName             ← private mixin                     │
│                                                             │
│  class Example {                                            │
│    int _field;          ← private field                     │
│    void _method() {}    ← private method                    │
│    Example._ctor();     ← private constructor               │
│    static int _s = 0;   ← private static                    │
│    int get _prop => 0;  ← private getter                    │
│  }                                                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Summary

ItemValue
Private syntaxLeading underscore _
Privacy scopeLibrary, not class
EnforcementCompile-time by analyzer and compiler
Library definitionA file without part of
Part filesShare the library’s privacy scope
part directiveIncludes a file in the library
part of directiveDeclares a file as part of a library
Private constructorClassName._name()
Private extensionextension _Name on Type
Private mixinmixin _Name
Runtime visibilityPrivate members still exist at runtime

Key takeaways:

  • Privacy in Dart is library-scoped, not class-scoped. A leading underscore makes an identifier private to the library where it is declared. All code in that library can access it, including other classes, mixins, and extensions.
  • A library is a file with no part of directive. A standalone .dart file is its own library. Its private members are inaccessible from every other file unless those files are declared as parts of the same library.
  • The part and part of directives extend the library across files. A library file can include other files with part. Those files declare part of the library and share its privacy scope. Private members are accessible across all files in the library.
  • Privacy is a compile-time restriction. The analyzer and compiler reject attempts to access private members from outside the library. The members still exist at runtime and can be reached through reflection in some environments.
  • Private constructors control instantiation. A private named constructor ClassName._internal() can only be called from within the library. This is the pattern for singletons, factories, and controlled creation.
  • Underscore applies to all declarations. Top-level variables, functions, classes, type aliases, enums, extensions, mixins, and all class members can be made private with a leading underscore.
  • The library directive is optional. In modern Dart, a file becomes a library automatically. The library; directive is used only when attaching metadata or documentation.
  • Testing private members requires being in the same library. The standard approach is to use part to include test code in the library, or to test through the public API. There is no way to access private members from a separate library.

Remember: The underscore is Dart’s entire visibility system, and it is scoped to the library rather than the class. This is different from most object-oriented languages, and it takes time to internalize. Two classes in the same file share private members. Two classes in different files do not, unless those files are connected by part and part of. The model is simple, but its implications for code organization are significant: group related code that shares private state into the same library, use part files when splitting is necessary, and expose a clean public API from each library. The underscore is not a hint or a convention; it is a rule the compiler enforces.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!