How do you turn a dotted config string like 'pkg.mod:ClassName' into the class object itself?
answer
- Two steps, not one
- Import the module, then getattr
- The separator decides the ambiguity
- Colon is the entry-point convention
- Allowlist before importing untrusted names
basics
~20 sSplit 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 sSplit 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 linesimport 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
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.
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.
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.
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