Visibility Modifiers
Reviewed & published by Brayan K
Visibility modifiers are keywords — public, private, protected, and internal — that control which parts of your code can see and use a declaration.
Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
Used well, they enforce encapsulation: hiding internal state so an object can only be changed through the safe operations you choose to expose.
What You'll Learn in This Lesson
- public as the default visibility
- private inside a class versus at the top level
- protected and how subclasses see it
- internal for module-wide visibility
- Encapsulating state with private fields
- Exposing read-only access through getters
1️⃣ public (Default) & Top-Level private
If you write no modifier, a declaration is public — visible everywhere. At the top level of a file, private limits a function or property to that single file, which is great for small helpers you don't want leaking out.
// Top-level declarations are PUBLIC by default.
fun greet() = "Hello" // visible everywhere
// 'private' at top level limits to THIS file:
private fun secret() = "shh" // only this file can call it
fun main() {
println(greet()) // -> Hello
println(secret()) // -> shh (same file, so allowed)
}2️⃣ private Inside a Class
Inside a class, private hides a member from the outside world — only the class's own methods may touch it. This is the heart of encapsulation: callers interact through public methods like deposit, never with the raw field.
class BankAccount {
// public (default): anyone can read it
val owner = "Ada"
// private: only this class can touch it
private var balance = 0
fun deposit(amount: Int) {
balance += amount // allowed: same class
}
fun showBalance() {
println("Balance: $balance") // allowed: same class
}
}
fun main() {
val acc = BankAccount()
println(acc.owner) // -> Ada (public)
acc.deposit(100)
acc.deposit(50)
acc.showBalance() // -> Balance: 150
// acc.balance // ❌ won't compile: private
}3️⃣ Encapsulation & protected
A common pattern is a private backing field exposed through a read-only getter: the outside can read the value but never assign it, so all changes flow through your controlled methods.
class Counter {
// The data is private; the outside world can only READ it
// through a public getter, never set it directly.
private var count = 0
val value: Int
get() = count // public read-only view
fun increment() {
count++ // controlled mutation
}
}
fun main() {
val c = Counter()
c.increment()
c.increment()
c.increment()
println(c.value) // -> 3
// c.value = 99 // ❌ no setter: read-only
}The protected modifier is like private, but it also lets subclasses see the member:
open class Animal {
protected val species = "generic" // visible to subclasses
fun reveal() = "I am a $species"
}
class Dog : Animal() {
fun describe(): String {
// 'protected' member is visible here in the subclass:
return "A $species that barks"
}
}
fun main() {
val d = Dog()
println(d.reveal()) // -> I am a generic
println(d.describe()) // -> A generic that barks
// d.species // ❌ protected: not visible outside
}Your turn. Fill in the ___ blank, then run and compare.
// 🎯 YOUR TURN — replace each ___ then run main()
class Safe {
// 1) Make 'pin' inaccessible from outside the class
___ val pin = 1234 // 👉 private
fun check(guess: Int): Boolean {
return guess == pin
}
}
fun main() {
val s = Safe()
println(s.check(1234)) // 👉 expected: true
println(s.check(0)) // 👉 expected: false
}Mini-Challenge: Encapsulated Thermostat
Hide the temperature behind a private field and expose only safe operations. Warming once (+5) then cooling once (-5) returns to the start.
// 🎯 MINI-CHALLENGE: an encapsulated thermostat.
// Save as Thermostat.kt and run main().
//
// 1. Keep the temperature in a PRIVATE var, starting at 20.
// 2. Expose a public read-only 'current: Int' via a getter.
// 3. Add warmer() that raises it by 5 and cooler() that lowers it by 5.
// 4. In main: warm once, cool once, then print current.
//
// ✅ Expected output:
// Temperature: 20
fun main() {
// your code here
}Common Errors (and the fix)
- ❌ "Cannot access 'balance': it is private in 'BankAccount'". ✅ Add a public method or getter instead of touching the field directly.
- ❌ Marking everything public and exposing raw mutable fields. ✅ Keep state private; expose controlled operations.
- ❌ Expecting protected to be visible to unrelated callers. ✅ protected is only for the class and its subclasses.
- ❌ Assigning to a property that has only a getter. ✅ Add a setter, or mutate through a method like increment().
Pro Tips
- 💡 Default to private for fields and open up only what callers truly need.
- 💡 private set: var x = 0; private set gives public reads but private writes.
- 💡 internal is your friend in libraries — public inside the module, hidden outside.
📋 Quick Reference — Visibility
| Modifier | Who can see it |
|---|---|
| public | Everywhere (the default) |
| private (class) | Only the same class |
| private (top level) | Only the same file |
| protected | The class and its subclasses |
| internal | The same module |
🎉 Lesson Complete!
- ✅ public is the default — visible everywhere
- ✅ private means same class, or same file at the top level
- ✅ protected adds visibility to subclasses
- ✅ internal limits visibility to the module
- ✅ Encapsulate with private fields and read-only getters
Practice quiz
Which visibility is the default in Kotlin when no modifier is written?
- private
- internal
- protected
- public
Answer: public. Declarations are public by default, visible everywhere.
How many visibility modifiers does Kotlin have?
- Two
- Four
- Three
- Five
Answer: Four. They are public, private, protected, and internal.
Inside a class, what does private restrict a member to?
- Only code within that same class
- The whole module
- Subclasses too
- The whole file
Answer: Only code within that same class. A private class member is visible only inside that class.
At the top level of a file, what does private limit a declaration to?
- The whole package
- Subclasses
- The same file
- Nothing, it stays public
Answer: The same file. A private top-level declaration is visible only within that file.
What does protected add compared to private?
- Visibility to the entire module
- Visibility everywhere
- Nothing different
- Visibility to subclasses as well
Answer: Visibility to subclasses as well. protected is like private but also visible to subclasses.
What does internal restrict visibility to?
- A single class
- The same compilation module
- A single function
- Only subclasses
Answer: The same compilation module. internal makes a declaration visible everywhere within the same module.
What is the core idea of encapsulation?
- Hiding internal state behind a controlled interface
- Making every field public
- Avoiding classes entirely
- Using only top-level functions
Answer: Hiding internal state behind a controlled interface. Encapsulation hides state and exposes only safe operations.
How do you expose a private field as read-only to the outside?
- Make the field public
- Use a var with no type
- Provide a public getter with no setter
- Use protected
Answer: Provide a public getter with no setter. A private backing field plus a get-only property allows reads but not writes.
Can protected members be accessed by unrelated outside callers?
- Yes, always
- Yes, within the module
- Only via reflection
- No, only the class and its subclasses
Answer: No, only the class and its subclasses. protected is limited to the declaring class and its subclasses.
What does var x = 0; private set achieve?
- Makes x fully private
- Public reads but private writes
- Deletes the getter
- Makes x a val
Answer: Public reads but private writes. A private setter lets outside code read but not assign the property.
Continue this course
- Previous: Type Checks & Smart Casts (is, as)
- Next: Destructuring Declarations — Unpack pairs, data classes and map entries with componentN
- Quick reference: Kotlin cheat sheet
Frequently asked questions
What are Kotlin's four visibility modifiers?
They are public, private, protected, and internal. public is the default and means visible everywhere. private restricts a declaration to the class that contains it, or — at the top level — to the same file. protected is like private but also visible to subclasses. internal makes a declaration visible everywhere within the same compilation module (roughly, the same project module), but not to code that depends on that module.
How does private differ at the top level versus inside a class?
Inside a class, a private member is visible only to code within that same class. At the top level of a file — for a function, property, or class declaration — private instead means visible only within that file. So a private top-level helper function is a file-local utility, while a private class member is hidden behind the class's public methods.
Why is encapsulation worth the extra effort?
Encapsulation means hiding internal state behind a controlled interface. By making a field private and exposing only the operations that should be allowed — a read-only getter, a deposit method, an increment method — you guarantee the object can never reach an invalid state from the outside. Callers depend on your public methods, not on raw fields, so you can change the implementation later without breaking them.
When should I use internal instead of public?
Use internal for declarations that are public within your own module but should not be part of the API you expose to other modules or libraries that consume yours. It is ideal for shared helpers, framework plumbing, and implementation details you want freely available inside the project yet hidden from external callers. If you are writing a library, internal keeps your published surface small and stable.