Skip to main content
rule

nltk — Cursor rule: what it does, how to install

A Cursor rule for Python NLTK work, covering modular NLP code, typed functions, guarded data downloads, and tokenizer choices.

Install into Cursor · Manually: put the files in place All methods and platforms ↓
.cursor/rules/nltk.mdc
Open on GitHub

What nltk does

The rule targets Python files matching **/*.py. It recommends splitting tokenization, stopword removal, stemming, and lemmatization into small reusable units. NLTK objects should be passed into functions rather than kept as global state, while inputs and outputs should carry type hints.

It also covers setup and runtime choices. NLTK data should be checked before downloading, and RegexpTokenizer or ToktokTokenizer are suggested for large text collections instead of nltk.word_tokenize. WordNetLemmatizer works with POS tags; the example uses wordnet and averaged_perceptron_tagger. For large corpora, the rule treats nltk.word_tokenize as a poor default.

What it helps with

  • split an NLTK pipeline into testable stages
  • guard corpus downloads during setup
  • remove global state from text processing
  • choose a tokenizer for large corpora
  • supply POS tags for lemmatization

Example prompts for the agent

  • Review this Python NLTK code against the rule and split its text processing into functions.
  • Replace nltk.word_tokenize in this large-corpus workflow with a suitable tokenizer.
  • Fix lemmatization in this file by adding POS tags and checking the required NLTK data.

How to install nltk

Manually: put the files in place

.cursor/rules/nltk.mdc

Frontmatter: description, globs, alwaysApply

Step by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules

Questions about nltk

How do I install nltk in Cursor?

Copy the rule to .cursor/rules/nltk.mdc.

What does the nltk rule do?

It guides Python NLTK code toward modular processing stages, explicit dependency passing, type hints, guarded data downloads, and suitable tokenizer selection.

What does WordNetLemmatizer need for better results?

The rule recommends POS tags. Its example uses the wordnet and averaged_perceptron_tagger data packages.

Source text

nltk.mdc · CC0-1.0
---
description: This guide provides definitive, actionable best practices for writing maintainable, performant, and type-safe NLTK code in Python, emphasizing modern patterns and avoiding common pitfalls.
globs: **/*.py
---
# `nltk` Best Practices

NLTK remains a cornerstone for classic NLP tasks. Integrate it effectively with modern Python by following these guidelines.

## 1. Code Organization and Structure

Organize NLTK operations into modular, reusable components.

### 1.1 Modularize NLP Stages
Encapsulate each NLP step (tokenization, stemming, POS tagging) in a dedicated, pure function or small class. This promotes testability and allows easy swapping of implementations (e.g., NLTK to spaCy).

❌ **BAD: Monolithic Script**
```python
import nltk
from nltk.corpus import stopwords
from nltk.stem import PorterStemmer

text = "NLTK is a powerful library for natural language processing."
tokens = nltk.word_tokenize(text)
filtered_tokens = [word for word in tokens if word.lower() not in stopwords.words('english')]
stemmer = PorterStemmer()
stemmed_tokens = [stemmer.stem(word) for word in filtered_tokens]
print(stemmed_tokens)
```

✅ **GOOD: Modular Functions**
```python
import nltk
from nltk.corpus import stopwords
from nltk.stem import PorterStemmer
from typing import List

def tokenize_text(text: str) -> List[str]:
    """Tokenizes text into words."""
    return nltk.word_tokenize(text)

def remove_stopwords(tokens: List[str], lang: str = 'english') -> List[str]:
    """Removes common stopwords from a list of tokens."""
    stop_words = set(stopwords.words(lang))
    return [word for word in tokens if word.lower() not in stop_words]

def stem_tokens(tokens: List[str]) -> List[str]:
    """Applies Porter Stemming to a list of tokens."""
    stemmer = PorterStemmer()
    return [stemmer.stem(word) for word in tokens]

# Usage
document = "NLTK is a powerful library for natural language processing."
tokens = tokenize_text(document)
filtered = remove_stopwords(tokens)
stemmed = stem_tokens(filtered)
print(stemmed)
```

## 2. Common Patterns and Anti-patterns

### 2.1 Lazy Data Loading
Download NLTK corpora *once* during setup or on first run, not repeatedly. Guard `nltk.download()` calls.

❌ **BAD: Repeated Downloads**
```python
import nltk
# This will run every time the script executes
nltk.download('punkt')
nltk.download('stopwords')

def process_text(text: str) -> List[str]:
    # ...
    pass
```

✅ **GOOD: Lazy, Guarded Download**
```python
import nltk
import os

def ensure_nltk_data(corpus_name: str):
    """Downloads NLTK data if not already present."""
    try:
        nltk.data.find(f'corpora/{corpus_name}')
    except nltk.downloader.DownloadError:
        print(f"Downloading NLTK data: {corpus_name}...")
        nltk.download(corpus_name)
        print(f"Downloaded {corpus_name}.")

# In your application's entry point or setup script:
# ensure_nltk_data('punkt')
# ensure_nltk_data('stopwords')

# In your processing logic, assume data is available
def process_text(text: str) -> List[str]:
    # ...
    pass
```

### 2.2 Avoid Global State
Pass NLTK objects (like tokenizers, stemmers, lemmatizers) explicitly to functions. This improves testability and thread safety.

❌ **BAD: Global Stemmer Instance**
```python
from nltk.stem import PorterStemmer
_GLOBAL_STEMMER = PorterStemmer() # Global state

def process_document(text: str) -> List[str]:
    tokens = nltk.word_tokenize(text)
    return [_GLOBAL_STEMMER.stem(t) for t in tokens]
```

✅ **GOOD: Pass Objects Explicitly**
```python
from nltk.stem import PorterStemmer
from typing import List, Callable

# Define a type alias for clarity
StemmerFunc = Callable[[str], str]

def process_document(text: str, stemmer: StemmerFunc) -> List[str]:
    tokens = nltk.word_tokenize(text)
    return [stemmer(t) for t in tokens]

# Usage
porter_stemmer = PorterStemmer().stem # Get the bound method
processed_text = process_document("running ran runner", stemmer=porter_stemmer)
print(processed_text)
```

## 3. Performance Considerations

### 3.1 Choose the Right Tokenizer
For large corpora, `RegexpTokenizer` or `ToktokTokenizer` are significantly faster than `nltk.word_tokenize`.

❌ **BAD: Default `word_tokenize` for Large Scale**
```python
import nltk
# Slow for very large texts
tokens = nltk.word_tokenize(long_text_data)
```

✅ **GOOD: Faster Tokenizers for Production**
```python
from nltk.tokenize import RegexpTokenizer, ToktokTokenizer
from typing import List

def tokenize_regex(text: str) -> List[str]:
    """Uses RegexpTokenizer for faster word tokenization."""
    tokenizer = RegexpTokenizer(r'\w+')
    return tokenizer.tokenize(text)

def tokenize_toktok(text: str) -> List[str]:
    """Uses ToktokTokenizer for robust, faster word tokenization."""
    tokenizer = ToktokTokenizer()
    return tokenizer.tokenize(text)

long_text_data = "This is some very long text that needs efficient tokenization." * 1000
# Prefer one of these for performance
tokens_regex = tokenize_regex(long_text_data)
tokens_toktok = tokenize_toktok(long_text_data)
```

## 4. Common Pitfalls and Gotchas

### 4.1 Lemmatization Requires POS Tags
`WordNetLemmatizer` performs better when provided with a Part-of-Speech (POS) tag. Without it, it defaults to noun ('n').

❌ **BAD: Lemmatizing without POS**
```python
from nltk.stem import WordNetLemmatizer
lemmatizer = WordNetLemmatizer()
print(lemmatizer.lemmatize("running")) # Output: 'running' (incorrect for verb)
```

✅ **GOOD: Lemmatizing with POS Tags**
```python
from nltk.stem import WordNetLemmatizer
from nltk.corpus import wordnet
from typing import List, Tuple

def get_wordnet_pos(tag: str) -> str:
    """Map NLTK POS tag to WordNet POS tag."""
    if tag.startswith('J'): return wordnet.ADJ
    elif tag.startswith('V'): return wordnet.VERB
    elif tag.startswith('N'): return wordnet.NOUN
    elif tag.startswith('R'): return wordnet.ADV
    else: return wordnet.NOUN # Default to noun

def lemmatize_with_pos(tokens: List[str]) -> List[str]:
    """Lemmatizes tokens using WordNetLemmatizer with POS tags."""
    lemmatizer = WordNetLemmatizer()
    tagged_tokens = nltk.pos_tag(tokens) # Requires 'averaged_perceptron_tagger'
    return [lemmatizer.lemmatize(word, pos=get_wordnet_pos(tag)) for word, tag in tagged_tokens]

# Ensure data is downloaded:
# ensure_nltk_data('wordnet')
# ensure_nltk_data('averaged_perceptron_tagger')

tokens = ["The", "cats", "are", "running", "fast"]
lemmas = lemmatize_with_pos(tokens)
print(lemmas) # Output: ['The', 'cat', 'be', 'run', 'fast']
```

## 5. Type Hints

Always use type hints for NLTK functions, especially for inputs (`str`, `List[str]`) and outputs (`List[str]`, `nltk.Tree`). This improves readability, enables static analysis, and enhances IDE support.

```python
import nltk
from nltk.tree import Tree
from typing import List, Tuple

def pos_tag_sentence(tokens: List[str]) -> List[Tuple[str, str]]:
    """Performs Part-of-Speech tagging on a list of tokens."""
    # Requires 'averaged_perceptron_tagger'
    return nltk.pos_tag(tokens)

def chunk_entities(tagged_tokens: List[Tuple[str, str]]) -> Tree:
    """Chunks named entities from POS-tagged tokens."""
    # Requires 'maxent_ne_chunker' and 'words'
    return nltk.chunk.ne_chunk(tagged_tokens)

# Usage
sentence_tokens = ["Apple", "Inc.", "is", "headquartered", "in", "Cupertino", "."]
tagged = pos_tag_sentence(sentence_tokens)
entities = chunk_entities(tagged)
print(entities)
```

## 6. Virtual Environments and Packaging

### 6.1 Use Virtual Environments
Always develop NLTK projects within a virtual environment. This isolates dependencies and prevents conflicts.

```bash
# Create a virtual environment
python -m venv .venv
# Activate it
source .venv/bin/activate # Linux/macOS
.venv\Scripts\activate # Windows
# Install NLTK and other dependencies
pip install nltk
```

### 6.2 Manage Dependencies with `pyproject.toml`
Use `pyproject.toml` for modern dependency management and project configuration.

```toml
# pyproject.toml
[project]
name = "my-nltk-project"
version = "0.1.0"
dependencies = [
    "nltk>=3.8",
    "regex", # Often useful with NLTK
]

[tool.black]
line-length = 79

[tool.ruff]
line-length = 79
select = ["E", "F", "W", "I"] # Common rules
```

## 7. Testing Approaches

### 7.1 Unit Test Pure Functions
Since NLTK operations are often encapsulated in pure functions, unit testing is straightforward.

```python
# test_nlp_utils.py
import unittest
from unittest.mock import patch
from my_module import remove_stopwords, tokenize_text # Assuming your functions are in my_module

class TestNlpUtils(unittest.TestCase):
    def test_remove_stopwords(self):
        tokens = ["This", "is", "a", "test", "sentence", "for", "stopwords"]
        expected = ["test", "sentence", "stopwords"]
        # Mock stopwords to ensure test isolation
        with patch('nltk.corpus.stopwords.words', return_value=['this', 'is', 'a', 'for']):
            self.assertEqual(remove_stopwords(tokens), expected)

    def test_tokenize_text(self):
        text = "Hello, world!"
        # Mock nltk.word_tokenize to control behavior
        with patch('nltk.word_tokenize', return_value=['Hello', ',', 'world', '!']):
            self.assertEqual(tokenize_text(text), ['Hello', ',', 'world', '!'])

if __name__ == '__main__':
    unittest.main()
```

© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc

Similar IDE rules

All IDE rules for AI and ML →

From the same set240

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor