skip to content

questions

4

How do you turn a dotted config string like 'pkg.mod:ClassName' into the class object itself?

level: middleimportance: must knowfreq 55%

answer

  1. Two steps, not one
  2. Import the module, then getattr
  3. The separator decides the ambiguity
  4. Colon is the entry-point convention
  5. Allowlist before importing untrusted names

basics

~20 s

Split the string into a module part and an attribute part, call importlib.import_module on the module part, then getattr for the attribute. Prefer a colon separator so the split is unambiguous, validate the name against an allowlist, and raise a clear error naming the config key.

solid answer

~40 s

Split on the separator, import, then `getattr`: `module_name, _, attr = path.partition(":")`, `obj = getattr(importlib.import_module(module_name), attr)`. The colon form is worth insisting on because an all-dots string like `pkg.mod.Thing` is **ambiguous** — the resolver cannot tell a submodule from an attribute without trying an import and catching `ModuleNotFoundError`. Wrap the whole thing so failures are diagnosable: a missing module raises `ModuleNotFoundError`, a typo'd class raises `AttributeError`, and neither error mentions which config key was wrong unless you add it. If the string ever comes from outside your own deployment, check it against an allowlist first — resolving a name imports and therefore *executes* the module. Return the class, and let the caller instantiate it; a loader that also constructs the object conflates two failures.

code

python · 14 lines
python
import importlib

def load_object(path):
    module_name, sep, attr = path.partition(":")
    if not sep:
        raise ValueError(f"expected 'module:attribute', got {path!r}")
    module = importlib.import_module(module_name)
    try:
        return getattr(module, attr)
    except AttributeError:
        raise ImportError(f"{module_name!r} has no attribute {attr!r}") from None

print(load_object("collections:OrderedDict"))
print(load_object("json.decoder:JSONDecodeError"))

go deeper

for a junior

Know the shape of the answer: import the module part with importlib.import_module, then use getattr for the class name. Being able to write those two lines is the bar here.

for a middle

Explain why the separator matters, which exception each failure mode raises — ModuleNotFoundError, ImportError from the module body, AttributeError for the name — and why the colon convention removes the guesswork.

for a senior

Demonstrate operational judgement: resolve eagerly at startup and report every bad entry at once, wrap errors so they name the config key, validate the resolved object's shape, and never import a name derived from untrusted input.

for a principal

Own the contract itself — what the plugin target string is allowed to name, whether third parties may supply one, and the tradeoff between eager fail-fast resolution and lazy resolution that keeps startup cheap but defers failure.

### The two-step resolution Every dynamic-plugin story in Python reduces to the same two steps: import a module by string, then pull a name out of it. ```python import importlib def load_object(path): module_name, sep, attr = path.partition(":") if not sep: raise ValueError(f"expected 'module:attribute', got {path!r}") module = importlib.import_module(module_name) try: return getattr(module, attr) except AttributeError: raise ImportError(f"{module_name!r} has no attribute {attr!r}") from None ``` That is the whole mechanism. What separates a throwaway helper from one you can operate is everything around it. ### Why the colon, and why not all dots A string like `results.vendors.acme.AcmeParser` cannot be split correctly by inspection. `acme` might be a subpackage containing `AcmeParser`, or `acme.AcmeParser` might be a class attribute on a module named `acme`, or `AcmeParser` might be a nested class. `rsplit(".", 1)` guesses the most common shape and is wrong the moment somebody nests a class or renames a module into a package. The convention that avoids the guess is the one Python's own packaging metadata already uses for entry points: `module.path:attribute`, with the colon marking the boundary exactly. Adopt it for your own config keys and the ambiguity disappears. If you inherit an all-dots format, resolve it by walking: split off the last component, try to import the whole thing as a module first, and fall back to importing the prefix and `getattr`-ing the remainder — catching `ModuleNotFoundError` on the first attempt. Support dotted attributes after the colon (`mod:Outer.Inner`) with a small loop over `getattr` if you need nested classes. ### Failures, and making them readable Consider a clinical-lab result loader that reads a mapping of analyzer vendor to parser class from configuration and stands up one handler per vendor across a 17-service dependency graph. Four distinct failures wear near-identical tracebacks: 1. **The module does not exist** — `ModuleNotFoundError`, usually a typo or a distribution that was never installed in this environment. 2. **The module exists but its own import fails** — a plain `ImportError`, or any exception the module body raises. This one is routinely misdiagnosed as "plugin not installed" because a broad `except ImportError` swallows both. 3. **The attribute is missing** — `AttributeError`, a renamed or removed class. 4. **The object resolves but is the wrong kind of thing** — a function where a class was expected, or a class missing the method the caller will invoke. Nothing raises here; the failure surfaces much later. The fix for all four is the same: catch narrowly, re-raise with the offending **config key** and the **string** in the message, and chain with `from`. If you resolve many entries at startup, collect the failures and report them together rather than dying on the first — a deployment where three of seventeen vendors are misconfigured should tell you all three names in one run, not across three restarts. A `(4)` check is cheap: assert the resolved object is callable, or a subclass of your base class, at resolution time rather than at first use. ### Eager or lazy Resolving everything at startup gives you fail-fast: a bad string is a boot failure, not a 3 a.m. surprise, and startup is where you want import cost anyway. Resolving on first use keeps startup fast and lets a broken plugin harm only its own path, at the price of an intermittent-looking failure the first time an unusual vendor's results arrive. Most services want eager resolution with a clear summary of what loaded; a plugin host with dozens of rarely-used backends wants lazy, with the resolution result cached so the import cost is paid once. ### Security Resolution imports, and import executes. A config file inside your own deployment artifact is as trusted as your code, so an arbitrary string there is fine. A string that arrives from a request, a tenant-editable settings row, or an uploaded file is not: it is remote code execution over everything on `sys.path`. The mitigation is an allowlist — a dictionary of short logical names to fully-qualified targets that *you* wrote — with the untrusted input used only as a key. A prefix check ("must start with `ourapp.parsers.`") is weaker but far better than nothing. ### Return the class, not an instance Have the loader return the resolved object and let the caller construct it. Constructing inside the loader mixes import failure with constructor failure in one traceback, forces the loader to know constructor arguments, and makes the result impossible to cache. The one thing worth doing inside the loader is validating the *shape* of what came back.

  • Why is an all-dots string like a.b.C harder to resolve than a.b:C?
    Because dots separate both package components and attribute access, so the split point is a guess. `a.b.C` could be module `a.b` with attribute `C`, or module `a.b.C` itself. Resolving it means trying an import and catching `ModuleNotFoundError`, then falling back. A colon marks the boundary explicitly, which is exactly why packaging entry points use that form.
  • The resolved class is missing from the module. Which exception do you get, and what should the caller see?
    `getattr` raises `AttributeError`, which reads as an ordinary attribute bug and never mentions the config. Catch it and re-raise an `ImportError` or a custom configuration error naming the config key, the full target string and the module that was successfully imported, chaining with `from` so the original is preserved.
  • Should the loader instantiate the class it resolved?
    No. Return the class and let the caller construct it. Instantiating inside the loader merges import failures with constructor failures in one traceback, forces the loader to know constructor signatures, and prevents caching the resolution. Validating the shape of the resolved object — callable, or a subclass of the expected base — is worth doing there; constructing is not.
  • When is it acceptable to resolve a name that came from a user request?
    Essentially never as a raw name. Importing executes module code, so an arbitrary string is code execution over everything on sys.path. Use the untrusted value as a key into a mapping of logical names to targets you wrote yourself. A required prefix is a weaker fallback, and still lets an attacker reach anything under that prefix.

saying these in an interview costs you the question

  • Uses eval or exec on the config string
  • Assumes rsplit on a dot is always correct
  • Catches bare ImportError and reports 'plugin not installed'
  • Imports names taken straight from user input
  • Instantiates inside the loader and loses the error boundary
  • Cannot say which exception a missing attribute raises

context

open as a page

What does importlib.import_module do that a plain import statement cannot?

level: juniorimportance: should knowfreq 42%

basics

~20 s

importlib.import_module takes the module name as a string computed at runtime and returns that exact module object. The import statement needs the name spelled out in source code, and the low-level import builtin returns the top-level package instead of the submodule you asked for.

open as a page

How does importlib.metadata.entry_points discover installed plugins without importing them?

level: middleimportance: should knowfreq 26%

basics

~20 s

It reads the metadata files that installed distributions leave on sys.path, so discovery is a file scan rather than an import. Each result carries a name and a module:attribute target string; calling EntryPoint.load() is the step that actually imports and resolves it.

open as a page

Why can importlib.reload leave parts of a running program still executing the old code?

level: seniorimportance: should knowfreq 32%

basics

~20 s

importlib.reload re-executes the source into the same module object, so only attribute lookups through that module see new code. Names copied out by from-imports, existing instances and registered callbacks still reference the old function and class objects, which stay alive.

open as a page