| name | spacy |
| description | [Applies to: **/*.py] This guide provides definitive, opinionated best practices for writing maintainable, performant, and robust spaCy code in Python, focusing on modern patterns and avoiding common pitfalls. |
| source | cursor_mdc |
spaCy Best Practices
spaCy is the backbone of our NLP systems. Adhering to these guidelines ensures our pipelines are performant, reproducible, and easy to maintain.
1. Project Organization & Configuration
Always structure your spaCy projects using spacy project and define all pipeline settings in a declarative YAML config. This is non-negotiable for reproducibility and scalability.
✅ GOOD: Use spacy project and declarative configs.
Organize your pipeline logic in a dedicated src/pipeline/ package. Use project.yml to manage workflows and config.cfg for all spaCy pipeline settings.
from spacy.language import Language
from spacy.tokens import Doc
@Language.factory("my_custom_component")
def create_my_component(nlp: Language, name: str):
return MyCustomComponent(nlp, name)
class MyCustomComponent:
def __init__(self, nlp: Language, name: str):
self.nlp = nlp
self.name = name
def __call__(self, doc: Doc) -> Doc:
return doc
workflows:
train:
- "python -m spacy train config.cfg --output models/"
package:
- "python -m spacy package models/en_core_web_v1.0.0 ./dist --build wheel"
[nlp]
lang = "en"
pipeline = ["tok2vec", "ner", "my_custom_component"]
[components.my_custom_component]
factory = "my_custom_component"
❌ BAD: Ad-hoc scripts and hardcoded parameters.
Avoid scattering pipeline logic across multiple scripts or hardcoding model paths and hyperparameters. This makes experiments non-reproducible and deployment fragile.
import spacy
MODEL_PATH = "path/to/my/model"
THRESHOLD = 0.7
nlp = spacy.load(MODEL_PATH)
2. Type Hinting
Strictly use type hints for all spaCy objects (Language, Doc, Span, Token). This improves code readability, enables static analysis, and reduces runtime errors.
✅ GOOD: Comprehensive type hints.
Annotate function arguments and return types with precise spaCy types.
import spacy
from spacy.language import Language
from spacy.tokens import Doc, Span, Token
from typing import List
def process_document(nlp: Language, text: str) -> Doc:
"""Processes text with a spaCy pipeline."""
return nlp(text)
def extract_entities(doc: Doc) -> List[Span]:
"""Extracts named entities from a processed Doc."""
return list(doc.ents)
def get_token_lemma(token: Token) -> str:
"""Returns the lemma of a single token."""
return token.lemma_
❌ BAD: Untyped spaCy code.
Omitting type hints makes code harder to understand and refactor.
import spacy
def process_document(nlp, text):
return nlp(text)
def extract_entities(doc):
return list(doc.ents)
3. Performance Considerations
Optimize for speed and memory by disabling unused pipeline components and processing text in batches.
✅ GOOD: Efficient pipeline usage.
Use nlp.select_pipes for inference to only run necessary components. Process multiple documents with nlp.pipe.
import spacy
from spacy.language import Language
from spacy.tokens import Doc
from typing import Iterator
def analyze_text_batch(nlp: Language, texts: List[str]) -> Iterator[Doc]:
"""Processes a batch of texts, only running NER."""
with nlp.select_pipes(enable=["ner"]):
yield from nlp.pipe(texts, batch_size=50)
nlp_full = spacy.load("en_core_web_sm")
documents = ["Apple is looking at buying U.K. startup.", "Tim Cook is CEO of Apple."]
for doc in analyze_text_batch(nlp_full, documents):
print(f"Text: {doc.text}, Entities: {[(ent.text, ent.label_) for ent in doc.ents]}")
❌ BAD: Inefficient processing.
Loading full pipelines for simple tasks or processing documents one by one in a loop is wasteful.
import spacy
nlp = spacy.load("en_core_web_lg")
texts = ["Text 1", "Text 2", "Text 3"]
for text in texts:
doc = nlp(text)
4. Virtual Environments & Dependencies
Always use a virtual environment and pin your spacy version in requirements.txt. This prevents dependency conflicts and ensures consistent environments.
✅ GOOD: Pinned dependencies in a virtual environment.
python -m venv .venv
source .venv/bin/activate
pip install -U pip setuptools wheel
pip install -r requirements.txt
python -m spacy download en_core_web_sm
spacy==3.8.*
spacy-llm==0.8.*
❌ BAD: Global installs or unpinned versions.
Installing spacy globally or using spacy>=3.0 can lead to unexpected behavior and "works on my machine" issues.
spacy
5. Custom Components & Extension Attributes
Extend spaCy's Doc, Span, and Token objects using custom components and extension attributes. This keeps your custom logic integrated and accessible.
✅ GOOD: Custom components and extension attributes.
from spacy.language import Language
from spacy.tokens import Doc, Span, Token
if not Doc.has_extension("custom_sentiment"):
Doc.set_extension("custom_sentiment", default=0.0)
@Language.factory("sentiment_analyzer")
def create_sentiment_analyzer(nlp: Language, name: str):
return SentimentAnalyzer(nlp, name)
class SentimentAnalyzer:
def __init__(self, nlp: Language, name: str):
self.nlp = nlp
self.name = name
def __call__(self, doc: Doc) -> Doc:
score = sum(1 for token in doc if token.text == "good") - sum(1 for token in doc if token.text == "bad")
doc._.custom_sentiment = float(score)
return doc
[nlp]
pipeline = ["tok2vec", "ner", ]
[components.sentiment_analyzer]
factory =
nlp = spacy.load()
doc = nlp()
()
❌ BAD: Storing custom data separately or modifying core attributes.
Avoid maintaining parallel data structures or attempting to directly modify Doc attributes that are meant to be read-only.
import spacy
nlp = spacy.load("en_core_web_sm")
doc = nlp("Some text.")
custom_data = {doc.text: {"sentiment": 0.5}}
6. Testing
Write focused unit tests for custom components and integration tests for pipeline behavior using pytest. Use minimal nlp objects for unit tests.
✅ GOOD: Targeted unit tests with pytest.
import pytest
import spacy
from spacy.language import Language
from spacy.tokens import Doc
@Language.factory("test_my_custom_component")
def create_test_my_component(nlp: Language, name: str):
return MyCustomComponent(nlp, name)
class MyCustomComponent:
def __init__(self, nlp: Language, name: str):
self.nlp = nlp
self.name = name
def __call__(self, doc: Doc) -> Doc:
doc.set_extension("processed_by_custom", default=True, force=True)
return doc
def test_my_custom_component_adds_extension():
nlp = spacy.blank("en")
nlp.add_pipe("test_my_custom_component")
doc = nlp("Hello world.")
assert doc._.processed_by_custom is True
def test_my_custom_component_returns_doc():
nlp = spacy.blank()
nlp.add_pipe()
doc = nlp()
(doc, Doc)
❌ BAD: Untested components or overly broad tests.
Avoid relying solely on manual testing or writing integration tests that load full, complex models for every small change.
import spacy
def test_full_pipeline_output():
nlp = spacy.load("en_core_web_lg")
doc = nlp("This is a test sentence.")
assert len(doc.ents) > 0
7. LLM Integration
For integrating Large Language Models, leverage the spacy-llm package. It provides a structured, component-based approach that aligns with spaCy's pipeline philosophy.
✅ GOOD: Use spacy-llm for structured LLM integration.
Define LLM components in your config.cfg just like any other spaCy component.
[nlp]
lang = "en"
pipeline = ["llm_ner"]
[components.llm_ner]
factory = "spacy.LLM"
task = {"@llm_tasks": "spacy.NER.v3"}
model = {"@llm_models": "spacy.GPT-3-5.v1"}
❌ BAD: Ad-hoc LLM calls outside the pipeline.
Directly calling LLM APIs in your application code bypasses spaCy's structured pipeline, making it harder to manage, test, and swap models.
import openai
import spacy
def process_with_llm_and_spacy(text: str):
response = openai.Completion.create(
model="text-davinci-003",
prompt=f"Extract entities from: {text}",
max_tokens=100
)
llm_entities = parse_llm_response(response)
nlp = spacy.load("en_core_web_sm")
doc = nlp(text)
return doc, llm_entities