skip to content

How do visibility modifiers apply to extension functions — what can a private/internal/member extension see and be seen by?

level: seniorimportance: should knowfreq 45%

answer

  1. Modifier applies to the extension, not the extended type
  2. private top-level = file-only; internal = module; public = everywhere
  3. Extension sees only receiver's public (+ same-module internal) members
  4. Member extension = dispatch + extension receiver, can use enclosing privates
  5. protected only for member extensions, not top-level

basics

~20 s

An extension follows normal visibility rules for where it is declared, not the type it extends. A top-level public extension is usable anywhere; a private one only in its file. Because it sits outside the class, it can only see that type's public members.

solid answer

~50 s

Extension *visibility* is governed by where the extension is declared, exactly like any other declaration — `public`, `internal`, `private`, `protected` apply to the extension function itself. A **top-level** extension marked `private` is visible only within its file; `internal` limits it to the module; `public` is everywhere. Crucially, the modifier does **not** change what the extension can *access* inside the receiver: since it is compiled as an outside static method, it sees only the receiver type's `public` (and `internal`, if in the same module) members — never `private` ones. You can also declare an extension as a **member** of another class/object; then it has two receivers (dispatch + extension), its visibility is that of the enclosing class's member, and it can use that class's private state. A common pattern is a `private fun Receiver.helper()` placed at file scope to keep a helper local while still reading like a method.

code

kotlin · 10 lines
kotlin
// File: report.kt
private fun String.indent(n: Int) = " ".repeat(n) + this // file-local helper

internal fun List<String>.report(): String =      // module-only
    joinToString("\n") { it.indent(2) }

// Member extension with two receivers
class Csv(private val sep: Char) {
    fun List<String>.row(): String = joinToString(sep.toString())
}

go deeper

for a junior

Knows public/private apply and that a private top-level extension is file-local.

for a middle

Explains internal scoping and that the extension only sees the receiver's public API.

for a senior

Separates callability from access, and describes member extensions with two receivers accessing enclosing privates.

for a principal

Uses visibility deliberately for API surface control (internal/file-private helpers, member extensions for encapsulated DSL scoping).

## Two separate questions: who can call it vs. what it can see Visibility for extensions splits into: 1. **Where the extension is visible (callable from).** 2. **What members of the receiver the extension can access.** ### 1. Callability follows the declaration site An extension is just a declaration, so the usual modifiers apply to *it*: ```kotlin public fun String.shout() = uppercase() + "!" // visible everywhere internal fun String.tag() = "[$this]" // same module only private fun String.trimQuotes() = trim('"') // this file only (top-level) ``` - **`public`** (default): callable wherever the function is imported. - **`internal`**: callable within the same Gradle/Maven **module**. - **`private`** at top level: callable only within the **same file** — a great way to keep a helper local while reading like a method. - **`protected`** is not allowed for top-level functions (there's no class to be protected within); it's only meaningful for member extensions. ### 2. Access is limited to the receiver's public API Because an extension compiles to a **static method outside the class**, it can read only what any outside caller could: ```kotlin class Account(private val balance: Long) { val isPositive get() = balance > 0 } fun Account.describe() = if (isPositive) "ok" else "low" // fun Account.raw() = balance // ERROR: balance is private to Account ``` It sees `public` members always, and `internal` members if declared in the same module. It can **never** touch `private` members of the receiver type. ## Member extensions: two receivers Declare an extension *inside* a class and it gains a **dispatch receiver** (the enclosing class instance) plus the **extension receiver**: ```kotlin class Formatter(private val sep: String) { fun List<String>.joined(): String = joinToString(sep) // can use Formatter's private sep } ``` Here the extension's visibility is the visibility of that member (`public` by default, but `private`/`protected` to the class are allowed), and it can access the enclosing class's private state because it is *inside* that class. Such member extensions are only callable where both receivers are in scope. ## Practical guidance - Use **`private` file-scope extensions** for local helpers to avoid polluting the public namespace. - Use **`internal`** for module-only utilities. - Remember: marking an extension `public` does **not** grant it access to the receiver's privates — visibility of the extension and access to the receiver are independent.

  • Can a public extension function read a private property of the type it extends?
    No. Being public controls who can call it, not what it can access; a top-level extension only sees the receiver's public (and same-module internal) API.
  • Why declare a top-level extension as private?
    To restrict it to the current file, keeping internal helper functions out of the public namespace while still calling them with dot syntax.
  • How does a member extension differ in visibility from a top-level one?
    It has the visibility of a class member (can be private/protected to the class), gains a dispatch receiver, and can access the enclosing class's private state.

saying these in an interview costs you the question

  • Thinking public extensions can access the receiver's private members
  • Believing the modifier changes the extended type's visibility
  • Saying protected works on a top-level extension
  • Not distinguishing 'who can call it' from 'what it can access'
  • Unaware of member extensions and their dispatch receiver

context