Ga naar hoofdinhoud

Python-circulaire import: oorzaken, oplossingen en best practices

Leer waardoor circulaire imports in Python ontstaan, hoe ze je code breken en ontdek beproefde strategieën om ze in echte projecten te detecteren, op te lossen en te voorkomen.
· 12 min lezen

Verken met AI

Het importsysteem van Python is ontworpen om simpel en intuïtief te zijn. In de meeste gevallen kun je je code over meerdere bestanden organiseren en alles samenbrengen met eenvoudige import-statements.

Maar zodra modules van elkaar afhankelijk worden, kun je op een frustrerend probleem stuiten: de circulaire import. Deze fouten duiken vaak onverwacht op, met verwarrende meldingen zoals:

ImportError: cannot import name 'X' from 'Y' (most likely due to a circular import)

In dit artikel bekijken we wat circulaire imports zijn, waarom ze ontstaan en hoe je ze oplost met simpele, effectieve strategieën. We laten ook zien hoe je je code zo ontwerpt dat je ze helemaal voorkomt, waardoor je projecten robuuster, beter onderhoudbaar en makkelijker te begrijpen worden.

Wat is een circulaire import in Python?

Een circulaire import ontstaat wanneer twee of meer Python-modules direct of indirect van elkaar afhankelijk zijn. Wanneer Python deze modules probeert te importeren, komt het in een lus terecht en kan het het importproces niet afronden.

Hier is een eenvoudig voorbeeld met twee modules:

# file: module_a.py
from module_b import func_b

def func_a():
    print("Function A")
    func_b()
# file: module_b.py
from module_a import func_a

def func_b():
    print("Function B")
    func_a()

Het uitvoeren van een van deze bestanden levert de volgende fout op:

ImportError: cannot import name 'func_a' from 'module_a' (most likely due to a circular import)

Wat gebeurt hier? Python begint met het laden van module_a, dat module_b importeert. Maar vervolgens probeert module_b opnieuw module_a te importeren, en dat is voordat func_a is gedefinieerd. Omdat Python elke module maar één keer initialiseert, werkt het met een gedeeltelijk geladen versie van module_a, en mislukt de import.

Om het beter te begrijpen, kun je de importstroom zo zien:

Dit diagram laat zien hoe module_a.py importeert uit module_b.py, die op zijn beurt probeert terug te importeren uit module_a.py voordat die volledig is geladen. Dit creëert een circulaire afhankelijkheid en leidt tot een ImportError.Circulaire importstroom in Python. Afbeelding door de auteur. 

Dit creëert een afhankelijkheidslus. Omdat Python modules die al bezig zijn met importeren niet opnieuw laadt, stuit het op ontbrekende definities en wordt er een fout gegenereerd.

Typische foutmeldingen

Dit zijn enkele veelvoorkomende meldingen die wijzen op een probleem met circulaire imports:

  • ImportError: cannot import name 'X' from 'Y'

  • AttributeError: partially initialized module 'X' has no attribute 'Y'

Deze fouten zijn extra verwarrend omdat ze vaak diep in de call stack verschijnen, niet op de regel waar het daadwerkelijke probleem begon.

Waarom circulaire imports ontstaan

Circulaire imports zijn meestal niet bewust. Ze zijn een bijeffect van hoe modules zijn gestructureerd en hoe functies, klassen of constanten worden gedeeld. Door de oorzaken te begrijpen, kun je bestaande problemen oplossen en voorkomen dat je nieuwe introduceert naarmate je codebase groeit.

Veelvoorkomende oorzaken van circulaire imports

Verschillende ontwerppatronen en projectstructuren kunnen per ongeluk circulaire afhankelijkheden veroorzaken. Dit zijn de meest voorkomende scenario’s:

Wederzijdse afhankelijkheden tussen modules

Twee modules importeren elkaar om functionaliteit te gebruiken. Bijvoorbeeld, utils.py roept een functie aan in core.py, en core.py importeert ook iets uit utils.py. Geen van beide kan volledig laden zonder de ander.

Top-level imports die te vroeg afgaan

Als een klasse of functie op topniveau wordt geïmporteerd (dus buiten een functie of methode), wordt die uitgevoerd zodra de module wordt geïmporteerd. Dat kan problemen geven als die top-level import een circulaire verwijzing triggert.

Klassen die elkaar nodig hebben

In objectgeoriënteerd design is het gebruikelijk dat de ene klasse de andere nodig heeft. Bijvoorbeeld een User-klasse die een Profile-klasse gebruikt, en andersom. Als beide in aparte modules staan en op topniveau worden geïmporteerd, ontstaat een circulaire import.

Onduidelijke modulegrenzen

Naarmate projecten groeien, kan code strak gekoppeld raken tussen modules. Als verantwoordelijkheden niet duidelijk gescheiden zijn, beland je al snel in een kluwen van onderlinge imports.

Impliciete imports via frameworks of plugins

Soms ontstaan circulaire imports via externe libraries, vooral bij frameworks met plugins of autodiscovery. Die kunnen indirect imports triggeren en circulaire problemen veroorzaken die lastiger te traceren zijn.

Praktijkvoorbeeld: physics.py en entities/post.py

Stel, je bouwt een eenvoudige game-engine. Je hebt twee modules:

  • physics.py regelt zwaartekracht- en botsingslogica.
  • entities/post.py definieert speler- en vijandklassen, die functies uit physics.py gebruiken.

De code kan er zo uitzien:

# file: entities/post.py
from physics import apply_gravity  # Top-level import
class Player:
    def __init__(self, mass):
        self.mass = mass
    def update(self):
        apply_gravity(self)
# file: physics.py
from entities.post import Player  # Top-level import creates circular dependency
def apply_gravity(entity):
    if isinstance(entity, Player):
        print(f"Applying gravity to player with mass {entity.mass}")

Als je nu Player probeert te importeren of de gamelogica draait, krijg je de volgende fout:

Traceback (most recent call last):
  File "entities/post.py", line 1, in <module>
    from physics import apply_gravity
  File "physics.py", line 1, in <module>
    from entities.post import Player
ImportError: cannot import name 'Player' from 'entities.post' (most likely due to a circular import)

Dit is wat er gebeurt:

  1. entities/post.py wordt eerst geïmporteerd en probeert apply_gravity() uit physics.py te laden.

  2. physics.py begint te laden en probeert de klasse Player uit entities/post.py te importeren.

  3. Maar Player is nog niet gedefinieerd, want Python is nog bezig met entities/post.py!

Dit creëert een circulaire lus waarbij elk bestand wacht tot het andere klaar is met laden. Python eindigt met een gedeeltelijk geïnitialiseerde module en gooit een ImportError.

Dit soort indirecte circulaire afhankelijkheid komt vaak voor in grotere projecten waar logica over modules is verdeeld. Gelukkig zijn er, zoals we zo zien, verschillende manieren om dit op te lossen en te voorkomen.

Problemen veroorzaakt door circulaire imports

Circulaire imports veroorzaken niet alleen importfouten, ze beïnvloeden je code ook op subtielere manieren. Van verwarrende foutmeldingen tot langetermijnproblemen in de architectuur: dit kun je tegenkomen als circulaire afhankelijkheden onopgemerkt blijven.

ImportErrors en AttributeErrors

Het meest directe effect van een circulaire import is een fout tijdens het laden van modules. Die nemen meestal twee vormen aan:

  • ImportError: cannot import name 'X' from 'Y': Python probeert een naam te benaderen die nog niet is gedefinieerd omdat de module niet is uitgeladen.

  • AttributeError: partially initialized module 'X' has no attribute 'Y': Python importeerde de module, maar de functie of klasse die je wilt gebruiken bestaat nog niet door de importlus.

Deze fouten zijn frustrerend omdat ze vaak naar het symptoom wijzen (bijv. een ontbrekende functie) in plaats van naar de oorzaak (de circulaire import).

Moeilijk te debuggen afhankelijkheden

Circulaire imports creëren vaak onzichtbare ketens van afhankelijkheden in je codebase. Een bug in de ene module lijkt te ontstaan in een andere, waardoor debuggen veel lastiger wordt. Je kijkt mogelijk in het verkeerde bestand, zonder te beseffen dat de oorzaak een circulaire verwijzing is, meerdere lagen diep.

Dit is extra problematisch in grote applicaties waar één kleine import bovenaan een bestand een hele rits problemen kan veroorzaken.

Slechte leesbaarheid en onderhoudbaarheid

Circulaire imports zijn meestal een teken dat modules te veel doen of te strak gekoppeld zijn. Als bestanden elkaar in een lus nodig hebben, wordt het lastiger om te begrijpen waar logica leeft en hoe delen van je code samenwerken.

Op termijn maakt dit de code moeilijker te onderhouden. Nieuwe teamleden (of jijzelf later) moeten extra tijd besteden aan het ontwarren van de onderlinge afhankelijkheden voordat ze iets aanpassen.

Potentiële prestatieknelpunten

In sommige gevallen proberen developers circulaire imports te “oplossen” met dynamische of herhaalde imports via technieken als importlib of lokale imports binnen functies. Hoewel dat kan werken, kan het ook kleine prestatiepenalties veroorzaken door herhaalde resolutie of uitgesteld laden tijdens runtime, vooral als het vaak gebeurt binnen strakke loops of grootschalige applicaties.

Een architectonische rode vlag

Belangrijker nog: circulaire imports duiden vaak op een dieperliggend ontwerpprobleem. Ze suggereren dat je code geen duidelijke scheiding van verantwoordelijkheden heeft. Modules die te sterk van elkaar afhankelijk zijn, zijn lastiger te testen, te schalen en te refactoren. Met andere woorden: circulaire imports zijn niet alleen bugs, het zijn code smells.

In de volgende sectie bekijken we hoe je circulaire imports oplost met praktische strategieën zoals lokale imports, refactoren en dynamisch laden. Deze fixes stoppen niet alleen de fouten, maar helpen ook je architectuur op te schonen. Samengevat, om de praktische effecten van circulaire imports en waarom het meer is dan alleen irritatie te duiden, volgt hier een kort overzicht van de belangrijkste problemen die ze veroorzaken:

Probleem

Omschrijving

ImportError / AttributeError

Python kan de import niet afronden omdat de module slechts gedeeltelijk is geladen. Dit resulteert vaak in cryptische foutmeldingen.

Moeilijk te debuggen

Fouten verschijnen vaak ver van het echte probleem, waardoor root-cause-analyse lastig is, zeker in grote codebases.

Slechte onderhoudbaarheid

Circulaire afhankelijkheden maken refactoren of uitbreiden moeilijker. Modules raken strak gekoppeld en lastiger te begrijpen.

Prestatie-overhead

Workarounds zoals dynamische imports of lazy loading kunnen kleine maar onnodige vertragingen tijdens runtime introduceren.

Architecturale geur

Circulaire imports wijzen op een gebrek aan scheiding van verantwoordelijkheden en een zwakke projectstructuur, waardoor het systeem fragieler wordt.

Deze signalen herkennen is de eerste stap. Laten we nu bekijken hoe je circulaire imports oplost met strategieën die zowel de functionaliteit als de structuur verbeteren.

Hoe je circulaire imports oplost

Zodra je een circulaire import hebt gevonden, is het goede nieuws dat er meerdere effectieve manieren zijn om het op te lossen. Laten we de meest betrouwbare technieken doornemen.

Refactor je modules

Vaak ontstaan circulaire imports omdat modules te veel doen of te strak verbonden zijn. Een van de schoonste fixes is je code herstructureren door:

  • Gedeelde functionaliteit te verplaatsen naar een derde bestand (bijv. common.py, utils.py of base.py)

  • Twee onderling afhankelijke modules samen te voegen als ze logisch één geheel vormen.

Kijk naar dit voorbeeld waarin gedeelde logica wordt uitgehaald:

# file: common.py
def apply_gravity(entity):
    print("Gravity applied to", entity)

# file: physics.py
from common import apply_gravity

# file: entities/post.py
from common import apply_gravity

Door apply_gravity() naar common.py te verplaatsen, kunnen zowel physics.py als entities/post.py het importeren zonder van elkaar afhankelijk te zijn.

Gebruik lokale of luie imports

In plaats van bovenaan een bestand te importeren, plaats je de import in de functie of methode die hem daadwerkelijk gebruikt. Zo stel je de import uit tot de functie wordt aangeroepen, nadat alle modules zijn geladen. Hier is een voorbeeld van een luie import in een methode:

# file: physics.py
def apply_gravity(entity):
    from entities.post import Player  # Local import
    if isinstance(entity, Player):
        print("Applying gravity")

Dit werkt goed wanneer de import alleen in specifieke situaties nodig is. Voeg wel een comment toe die uitlegt waarom de import daar staat.

Gebruik 'import module' in plaats van 'from module import …'

Met import module stel je naamresolutie uit tot runtime, wat kan helpen om vroege lookups te vermijden die circulaire imports triggerren.De onderstaande code is een voorbeeld van een directe import die een vroege lookup veroorzaakt:

from physics import apply_gravity  # May cause a circular import

Een geschiktere aanpak is uitgestelde attribuuttoegang:

import physics
def update():
    physics.apply_gravity()

Deze methode is simpel en vaak effectief, zeker als je een functie of klasse slechts af en toe nodig hebt.

Verplaats imports naar de onderkant

In sommige situaties lost het probleem zich op door de importstatement simpelweg aan het einde van het bestand te plaatsen, na de klasse-/functiedefinities. Een goed voorbeeld:

# file: module_a.py
def func_a():
    print("Function A")
from module_b import func_b  # Import after definitions

Dit werkt alleen als de geïmporteerde naam niet nodig is tijdens de initialisatie van de module, dus gebruik het met zorg.

Gebruik importlib voor dynamische imports

Met de ingebouwde module importlib kun je modules programmatisch laden. Dit is vooral handig voor optionele plugins of runtime-logica waarbij imports uitgesteld moeten worden.

Hier is een voorbeeld met importlib.import_module

import importlib
def get_player_class():
    entities = importlib.import_module("entities.post")
    return entities.Player

Deze methode vermijdt top-level imports volledig en houdt afhankelijkheden flexibel. Het is een goede keuze voor pluginsystemen, extensies of dynamische routinglogica.

Met meerdere strategieën om uit te kiezen, is het handig om ze naast elkaar te vergelijken. Hier is een snelle referentie om te bepalen welke oplossing het beste past bij jouw situatie:

Oplossing

Wanneer te gebruiken

Hoe het helpt

Voorbeeld

Refactor je modules

Wanneer twee modules afhangen van gedeelde logica

Verplaatst gedeelde code naar een neutrale plek en doorbreekt de lus

Extraheer naar common.py

Gebruik lokale/luie imports

Wanneer de import alleen binnen een functie of methode nodig is

Stelt de import uit tot runtime, nadat alle modules geladen zijn

from module import X binnen een functie

Gebruik import module in plaats van from module import …

Wanneer je een paar functies of klassen nodig hebt

Stelt naamresolutie uit en vermijdt voortijdige toegang

importeer module_name en dan module_name.X

Verplaats imports naar de onderkant

Wanneer geïmporteerde namen niet nodig zijn tijdens initialisatie

Laat de module zichzelf volledig definiëren vóór het importeren

Plaats from module import X aan het einde van het bestand

Gebruik importlib voor dynamische imports

Bij optionele modules, plugins of runtime-logica

Geeft volledige controle over wanneer en hoe een module wordt geïmporteerd

importlib.import_module("module")

Hoe je circulaire imports voorkomt

Het oplossen van circulaire imports is nuttig, maar ze voorkomen is nog beter. Een goed georganiseerde codebase met duidelijk gedefinieerde grenzen tussen modules zal deze problemen veel minder snel hebben.

Dit zijn beproefde manieren om circulaire imports in Python-projecten te vermijden.

Plan je module-architectuur vroeg

Circulaire imports ontstaan vaak door een zwakke projectstructuur. Voorkom dit door je module-indeling te plannen voordat je gaat implementeren. Stel jezelf deze vragen:

  • Heeft elke module één duidelijke verantwoordelijkheid?
  • Scheid je logica per concern (bijv. modellen, services, utilities)?
  • Kunnen bepaalde modules worden samengevoegd of geabstraheerd?

Gebruik een top-down aanpak of visuele tool om te schetsen hoe modules moeten samenwerken voordat je gaat coderen.

Pas architectuurpatronen toe

Patronen zoals model-view-controller (MVC) of gelaagde architectuur voorkomen van nature circulaire imports door een hiërarchie in afhankelijkheden af te dwingen.

  • Controllers mogen afhangen van modellen, maar niet andersom.
  • Views hangen af van controllers, maar importeren geen businesslogica direct.

Deze top-down flow houdt je afhankelijkheden schoon en eendirectioneel.

Importeer geen implementatiedetails

Probeer alleen te importeren wat een module publiekelijk aanbiedt, niet de interne helpers of klassen. Stel in plaats van diep in een andere module een klasse te importeren, een schone API bloot op topniveau van die module.

# Good
from auth import authenticate_user  # Clean interface

# Risky
from auth.utils.token_handler import generate_token  # Fragile and tightly coupled

Deze praktijk maakt je modules makkelijker te refactoren zonder verborgen afhankelijkheden te creëren.

Wees voorzichtig met relatieve imports

Hoewel relatieve imports (from .module import X) je code schoner kunnen maken, vergroten ze in diep geneste packages ook de kans op circulaire verwijzingen.

Gebruik ze spaarzaam en alleen wanneer ze de leesbaarheid duidelijk verbeteren. In grote applicaties hebben absolute imports met duidelijk gedefinieerde modulepaden de voorkeur.

Gebruik dependency injection

Als twee modules gedeelde functionaliteit nodig hebben, overweeg dan om die afhankelijkheid te injecteren in plaats van te importeren. Hier is een voorbeeld:

# Instead of importing directly
def run_simulation():
    from physics import apply_gravity
    apply_gravity()

# Use dependency injection
def run_simulation(apply_gravity_fn):
    apply_gravity_fn()

Dit houdt je modules losjes gekoppeld en maakt unit-testen ook makkelijker.

Visualiseer je importgrafiek

Gebruik tools om te inspecteren en te visualiseren hoe modules van elkaar afhankelijk zijn:

  • pydeps: Genereert afhankelijkheidsgrafieken van je project.

  • snakeviz: Visualiseert runtime-profielen (handig als luie imports prestaties beïnvloeden).

  • pipdeptree: Inspecteert afhankelijkheden van third-party packages.

Regelmatig deze grafieken bekijken helpt om circulaire imports te vinden voordat ze echte problemen veroorzaken.

Gebruik code reviews als vangnet

Maak de importstructuur tot slot onderdeel van je code-review-checklist. Architectuurproblemen vroeg signaleren is veel makkelijker dan later kapotte imports debuggen. Even snel naar de importboom kijken kan je uren frustratie schelen.

Conclusie

Circulaire imports zijn zo’n Python-valkuil die in eerste instantie mysterieus lijkt, maar zodra je begrijpt wat er onder de motorkap gebeurt, worden ze veel eenvoudiger te diagnosticeren en te fixen.

In de kern zijn circulaire imports een bijeffect van hoe modules zijn gestructureerd en hoe ze samenwerken. Ze duiken vaak op in groeiende projecten waarin logica strak gekoppeld raakt of verantwoordelijkheden over bestanden vervagen. Maar ze zijn ook een nuttig signaal, een kans om even afstand te nemen, je architectuur te herevalueren en je codebase te vereenvoudigen.

Wil je je Python-skills verder aanscherpen, bekijk dan Writing Efficient Python Code om meer te leren over het ontwerpen van onderhoudbare code. Je kunt ook een sterke basis leggen met onze cursus Introduction to Python, of dieper duiken in modulair ontwerp met Object-Oriented Programming in Python - dit zijn allemaal goede opties.

Samuel Shaibu

Auteur

Samuel Shaibu

Ervaren dataprofessional en schrijver die graag ambitieuze experts in de dataruimte wil versterken.

FAQs

Waardoor ontstaan circulaire imports in Python?

Circulaire imports ontstaan wanneer twee of meer modules van elkaar afhankelijk zijn en zo een lus creëren die voorkomt dat Python een van beide modules volledig kan laden.

Hoe kan ik circulaire imports vroegtijdig detecteren?

Zoek naar wederzijdse imports tussen bestanden of gebruik tools zoals pydeps om de importgrafiek van je project te visualiseren.

Wat is de snelste manier om een circulaire import op te lossen?

Gedeelde logica refactoren naar een aparte module of een lokale import binnen een functie gebruiken zijn vaak de snelste oplossingen.

Is het slecht om importlib te gebruiken om circulaire imports op te lossen?

Het is prima voor dynamische of optionele imports, maar op de lange termijn is het beter om je code te herstructureren zodat je circulaire afhankelijkheden helemaal vermijdt.

Hoe kan ik circulaire imports in mijn project voorkomen?

Plan je modulestructuur vroeg, houd je aan een duidelijke scheiding van verantwoordelijkheden en gebruik dependency injection of services-lagen wanneer modules moeten samenwerken.

Onderwerpen

Gerelateerd