What do public, package and internal imports change, and why can a client use members of a module it never imported?

16.9.2 · L5 · Interview

Predict first

Does App see Dep’s extension member without importing Dep, with and without MemberImportVisibility?

// === run.sh ===
W=$(mktemp -d); cd $W; mkdir -p Sources/Dep Sources/Core Sources/App
manifest() { cat > Package.swift <<E
// swift-tools-version: 6.0
import PackageDescription
let package = Package(name: "P", targets: [
    .target(name: "Dep"),
    .target(name: "Core", dependencies: ["Dep"], swiftSettings: [.enableUpcomingFeature("InternalImportsByDefault")]),
    .executableTarget(name: "App", dependencies: ["Core"]$1),
])
E
}
echo 'public extension Int { var depValue: Int { self * 10 } }' > Sources/Dep/D.swift
printf 'import Dep\npublic func coreValue() -> Int { 4.depValue }\n' > Sources/Core/C.swift
printf 'import Core\nprint(coreValue(), 1.depValue)\n' > Sources/App/main.swift
manifest ""
echo "App never imports Dep, default lookup: $(swift build 2>&1 | grep -E 'error:|Build complete' | head -1)"
manifest ', swiftSettings: [.enableUpcomingFeature("MemberImportVisibility")]'
echo "with MemberImportVisibility: $(swift build 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | grep -E 'error: property' | head -1 | sed 's#.*error: ##; s# \[.*##')"
  • it compiles by default, and is an error "missing import of defining module Dep" with MemberImportVisibility
  • it fails in both cases
  • it compiles in both cases

Mental model. A public import is a window onto your supplier’s warehouse that clients can look through; an internal import is a door at the back that only you use.

Historically import Dep in a module was effectively public: its types could appear in your API, and, through a long-standing lookup quirk, extension members of Dep became visible to clients of your module even though they never imported Dep. The check shows it: Core uses import Dep (here with InternalImportsByDefault on), App imports only Core, and 1.depValue (an extension on Int from Dep) still compiles. Turning on the second upcoming feature, MemberImportVisibility (SE-0444), in App makes it an error: “property 'depValue' is not available due to missing import of defining module 'Dep'”. The first feature, SE-0409 access-level imports, lets you write public import, package import, internal import, fileprivate import or private import, and InternalImportsByDefault makes a bare import internal, so exposing a dependency in public API needs an explicit public import.

Why it matters for modular code. An internal import keeps a dependency an implementation detail: clients and the module’s .swiftinterface do not mention it, so you can later swap or drop it without breaking them. With library evolution, a public import in a binary framework forces every client to have that module available. The compiler helps: in an earlier run of the same package with public import Dep and no use in public declarations, it warned that the public import was not used in public declarations or inlinable code, which is a prompt to downgrade it. Adopt in this order: enable both features per target (they are per-target settings, and Swift 6 language mode does not turn them on by itself, as far as this check showed), fix missing imports in clients, then tighten each import to the lowest access that compiles. The cost is churn: files that relied on the leak must now import the module they actually use.

Extension members leaked through other modules’ imports until member import visibility was enabled.

Common traps

  • Relying on a transitive import for an extension. It works by accident and breaks when the middle module drops the dependency.
  • Making everything public import to silence errors. You expose dependencies that clients then rely on.

They might follow up with

  • What is package import for? A dependency shared by modules in one package, but invisible to clients outside it.
  • How does this connect to the module graph? Tighter imports mean fewer implicit edges, which keeps layering honest (Q368).

Related questions

  • 16.9.1 What does @inlinable change in a module’s .swiftinterface and in a client binary, and when is it worth the…
  • 16.7.6 How do you inspect a package and its dependency graph from the command line?
  • 16.1.2 How do internal, package and public work between modules, and what are the classic surprises?
  • 16.3.2 Why must the module dependency graph be acyclic, how do you layer it, and what does its shape do to build…

← What does @inlinable change in a module’s .swiftinterface and in a…