How do internal, package and public work between modules, and what are the classic surprises?
Predict first
Another target calls an internal method of a module it imports. What does the compiler say?
// === Package.swift ===
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "Demo",
products: [.library(name: "Core", targets: ["Core"]), .executable(name: "app", targets: ["App"])],
targets: [
.target(name: "Core"),
.executableTarget(name: "App", dependencies: ["Core"]),
]
)
// === Sources/Core/Core.swift ===
public struct Greeter {
public init() {}
public func hello() -> String { "hello" }
func hidden() -> String { "hidden" }
package func pkg() -> String { "pkg" }
}
// === Sources/App/main.swift ===
import Core
print(Greeter().hidden())
- It compiles
- It is an error: inaccessible due to 'internal' protection level
Predict first
The same call through a package method:
// === Package.swift ===
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "Demo",
products: [.library(name: "Core", targets: ["Core"]), .executable(name: "app", targets: ["App"])],
targets: [
.target(name: "Core"),
.executableTarget(name: "App", dependencies: ["Core"]),
]
)
// === Sources/Core/Core.swift ===
public struct Greeter {
public init() {}
public func hello() -> String { "hello" }
func hidden() -> String { "hidden" }
package func pkg() -> String { "pkg" }
}
// === Sources/App/main.swift ===
import Core
print(Greeter().pkg())
- It compiles and prints pkg
- It is an error: not public
Predict first
A public struct with default values is constructed from another module. Does Plain() work?
// === Package.swift ===
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "Demo",
targets: [
.target(name: "Core"),
.executableTarget(name: "App", dependencies: ["Core"]),
]
)
// === Sources/Core/Core.swift ===
public struct Plain { public var x = 0 }
// === Sources/App/main.swift ===
import Core
print(Plain().x)
- Yes, the default initialiser is public
- No: the implicit initialiser is internal
Mental model. Internal is the staff-only room, package is the corridor shared by the staff of all shops in the same mall, and public is the shop window. A door you meant to open to customers needs a visible handle (the explicit public init).
Access control is enforced at the module boundary. internal (the default) means “visible inside this module”, so another target that imports it cannot see an unmarked declaration: the compiler reports “'hidden' is inaccessible due to 'internal' protection level”. public makes a declaration visible to importers, but importers can use it only as the declaration allows: a public class cannot be subclassed and its methods cannot be overridden outside the module, and open (classes and class members only) allows that. package (Swift 5.9) sits between: visible to every module in the same package but not to external clients, which is exactly what multi-target packages need for shared internals (a package func in Core is callable from the App target without becoming part of the public API). fileprivate and private narrow within a file or declaration. Because most code is internal, a module naturally has a small public surface that you choose deliberately.
The surprises. Implicit initialisers are internal: a public struct with default values still has an internal init() and memberwise initialiser, so another module that writes Greeter() fails with “initializer is inaccessible due to 'internal' protection level” until you declare public init() {} yourself. Protocol conformances and extensions are only as visible as the narrower of the type and protocol, and a public declaration cannot expose an internal type in its signature. Test targets can see internals with @testable import (in debug builds, with testability enabled), which is convenient but should not become a habit that skips designing the public surface. @inlinable and @usableFromInline expose bodies across modules for performance, and become part of the compatibility contract (see the library-evolution topic). Re-exports (@_exported import) leak dependencies through your API and should be avoided. Design rule: keep a feature module’s public interface to a builder or entry point and the models its callers need, make everything else internal, use package for cross-target sharing inside the package, and review the generated public interface (swift package diagnose-api-breaking-changes or module interface files) when you change it.
Unmarked declarations are internal to their module, so an importer cannot see them.
package access is visible to every target in the same package, without being public to outside clients.
Implicit and memberwise initialisers are internal even for public types. Declare public init() {} to expose one.
Common traps
- Forgetting
public initon a public type. The implicit initialiser is internal, so other modules cannot construct it. - Making everything
publicto silence errors. It enlarges the API you must maintain; preferpackageinside the package.
They might follow up with
- When do you use
package? To share code between targets of one package without exposing it to external clients. - What does
@testable importallow? Tests can see internal declarations, in builds with testability enabled.
Related questions
- 16.1.1 What are targets, products and modules in a Swift package, and what does a first Package.swift look like?
- 16.3.1 How do static and dynamic linking differ for an app, what do they cost at launch and in size, and what are…
- 15.3.1 What is a feature module, how do you draw module boundaries, and how does access control enforce them?
← What are targets, products and modules in a Swift package, and what…