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
| Declaration | Private Syntax | Accessible From |
|---|---|---|
| Top-level variable | _name | Same library |
| Top-level function | _name() | Same library |
| Class field | _name | Same library |
| Class method | _name() | Same library |
| Constructor | ClassName._name() | Same library |
| Static field | _name | Same library |
| Getter/setter | _name | Same library |
| Extension | _Name | Same library |
| Mixin | _Name | Same library |
Library Scope
| File Type | Privacy Scope |
|---|---|
| Standalone file | The file itself |
Library file with part | Library file + all parts |
part of file | The library it belongs to |
Multiple files without part | Each file is its own library |
What Underscore Does Not Do
| Misconception | Reality |
|---|---|
| Makes member private to class | Private to library |
| Hides member at runtime | Compile-time only |
| Works across packages | Works within a library only |
| Applies to libraries | Applies to identifiers |
Common Private Patterns
| Pattern | Purpose |
|---|---|
_internal constructor | Prevent direct instantiation |
_cache static map | Internal storage |
_name field | Encapsulated state |
_helper() function | Implementation detail |
_Result class | Internal 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
Cannot access _member from another file | File is a different library | Move both into same library with part |
| Test cannot reach private member | Test is in separate file | Use part or test through public API |
| Private member visible in another file | Both files are parts of same library | Ensure files are separate libraries |
| Underscore on a public API | Naming mistake | Remove underscore |
| Private constructor not callable | Called from another library | Use a public factory |
part of path mismatch | Wrong file path | Match the library file path exactly |
| Private extension not found | Extension is in different library | Move 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
| Item | Value |
|---|---|
| Private syntax | Leading underscore _ |
| Privacy scope | Library, not class |
| Enforcement | Compile-time by analyzer and compiler |
| Library definition | A file without part of |
| Part files | Share the library’s privacy scope |
part directive | Includes a file in the library |
part of directive | Declares a file as part of a library |
| Private constructor | ClassName._name() |
| Private extension | extension _Name on Type |
| Private mixin | mixin _Name |
| Runtime visibility | Private 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 ofdirective. A standalone.dartfile 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
partandpart ofdirectives extend the library across files. A library file can include other files withpart. Those files declarepart ofthe 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
librarydirective is optional. In modern Dart, a file becomes a library automatically. Thelibrary;directive is used only when attaching metadata or documentation. - Testing private members requires being in the same library. The standard approach is to use
partto 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!