The dozen facts behind most spaCy confusion — all confirmed on a live spacy 3.8.14 + en_core_web_sm install.
01Trailing underscore = string. token.pos_ → 'PROPN'; token.pos → 96 (a hash). Same for .lemma_ .dep_ .label_.
02Results are objects, not strings. nlp(text) returns a Doc of Tokens with attributes — no lists of tuples.
03One call runs everything. nlp(text) executes the whole pipeline; for many texts use nlp.pipe().
04doc[i] is a Token, doc[i:j] is a Span — slicing never gives a Python list.
05doc.ents is a tuple of Span, not a list you append to. Add rules via the EntityRuler.
06sm has no word vectors. .similarity() still runs but warns (W007) and is poor — use md/lg/trf.
07Download ≠ install. pip install spacy then python -m spacy download en_core_web_sm separately.
08Sentences need the parser or senter. doc.sents errors on a blank pipeline with neither.
09Matcher patterns are lists of dicts with UPPERCASE keys (LOWER, POS, OP); the pattern is wrapped in a list.
10Disable to go faster. nlp.pipe(texts, disable=['parser','ner']) skips work you don't need.
11spacy.explain(x) turns any tag, dep or label into plain English — keep it handy.
12pos_ = coarse (Universal), tag_ = fine (Penn). NOUN vs NN; custom data lives under doc._.