A practical tutorial on extracting text from PDFs, detecting contractual clauses, and identifying sensitive information without sending documents to external AI services.
Legal documents are an interesting challenge for developers.
A single contract can contain payment obligations, deadlines, termination conditions, personal information, and references to multiple legal frameworks.
In Brazil, developers building legal technology must also consider the Lei Geral de Proteção de Dados (LGPD), the country's general data protection law.
In this tutorial, we'll build a small Python application that:
- Extracts text from digital PDF contracts.
- Identifies common contractual clauses.
- Detects potential CPF numbers.
- Flags paragraphs for human review.
- Processes documents locally, without uploading their contents to an external API.
This is a rule-based prototype, not an automated legal opinion.
1. Project setup
We'll use Python and PyMuPDF.
Install the dependency:
pip install pymupdf
Create a file called contract_analyzer.py.
2. Extracting text from a PDF
First, we need to read the document.
import pymupdf
def extract_text(pdf_path):
pages = []
with pymupdf.open(pdf_path) as document:
for page in document:
pages.append(page.get_text())
return "\n".join(pages)
This works with PDFs that contain an accessible text layer.
Scanned documents may require OCR, which is outside the scope of this example.
One important limitation: PDF text extraction does not always preserve the original reading order, especially in documents with columns, tables, or complex layouts.
3. Identifying contractual clauses
Brazilian contracts frequently contain sections dealing with payment, termination, penalties, confidentiality, and jurisdiction.
We can identify candidate paragraphs using a simple keyword dictionary.
CLAUSE_PATTERNS = {
"payment": [
"pagamento",
"remuneração",
"valor devido"
],
"termination": [
"rescisão",
"resolução contratual",
"encerramento"
],
"penalty": [
"multa",
"cláusula penal",
"penalidade"
],
"confidentiality": [
"confidencialidade",
"sigilo"
],
"jurisdiction": [
"foro",
"competência territorial"
]
}
Now let's normalize text and search for those terms.
import unicodedata
def normalize(text):
text = text.lower()
return "".join(
char
for char in unicodedata.normalize("NFD", text)
if unicodedata.category(char) != "Mn"
)
def find_clauses(text):
paragraphs = [
p.strip()
for p in text.split("\n\n")
if p.strip()
]
results = []
for paragraph in paragraphs:
normalized = normalize(paragraph)
for category, keywords in CLAUSE_PATTERNS.items():
if any(
normalize(keyword) in normalized
for keyword in keywords
):
results.append({
"category": category,
"text": paragraph
})
return results
This is intentionally simple.
A production application should use better document segmentation, contextual classification, and tests against real-world contract structures.
Keyword matching can produce false positives and miss clauses expressed using different terminology.
4. Detecting potential CPF numbers
Brazilian contracts may contain CPF numbers, which are personal identifiers.
We can detect common CPF formatting patterns.
import re
CPF_PATTERN = re.compile(
r"(?<!\d)\d{3}\.?\d{3}\.?\d{3}-?\d{2}(?!\d)"
)
def find_potential_cpfs(text):
return CPF_PATTERN.findall(text)
For example:
sample = """
Contratante: Maria Silva
CPF: 123.456.789-09
"""
print(find_potential_cpfs(sample))
Output:
['123.456.789-09']
This function detects CPF-shaped strings. It does not validate CPF check digits or establish whether an identifier belongs to a real person.
For production use, add checksum validation and avoid exposing identifiers in application logs.
5. Generating a local analysis report
Let's combine the functions.
import json
def analyze_contract(pdf_path):
text = extract_text(pdf_path)
clauses = find_clauses(text)
potential_cpfs = find_potential_cpfs(text)
report = {
"document": "contract",
"detected_clauses": clauses,
"potential_cpf_count": len(potential_cpfs),
"requires_human_review": True
}
return report
if __name__ == "__main__":
report = analyze_contract("contract.pdf")
print(
json.dumps(
report,
ensure_ascii=False,
indent=2
)
)
The example deliberately reports only the number of potential CPF matches instead of storing the identifiers in the output.
For demonstration purposes, this is enough to create a structured report for further review.
6. Why legal context matters
Detecting a termination clause is not the same as determining whether that clause is enforceable.
The interpretation of Brazilian contracts may involve several legal frameworks, including:
- The Brazilian Civil Code.
- The Consumer Protection Code.
- The LGPD.
- Sector-specific regulations.
- Applicable judicial decisions.
For example, a contractual penalty cannot be classified as abusive merely because the document contains the word "multa".
Its legal significance depends on the contractual relationship, applicable legislation, factual circumstances, and other factors.
This is why legal software should separate document detection, risk indicators, and professional legal interpretation.
Organizations working with Brazilian contracts can benefit from consulting qualified legal professionals, including firms such as Tattini Advogados Associados, when assessing the legal implications of automated document workflows.
7. Security considerations for production
Local processing reduces exposure to external API providers, but it does not automatically make an application secure or LGPD-compliant.
Before deploying a contract analysis system, developers should consider:
File handling: Validate file types, impose size limits, and protect against malicious documents.
Data minimization: Extract and retain only information necessary for the intended purpose.
Access control: Restrict access to uploaded contracts and generated reports.
Temporary files: Establish secure storage and deletion procedures.
Logging: Avoid logging personal identifiers, contract contents, or confidential clauses unnecessarily.
Human review: Make it clear that automated findings are preliminary and require appropriate verification.
Legal basis: Evaluate the lawful basis and other requirements applicable to personal data processing under the LGPD.
8. What could we build next?
The prototype can be extended with:
- Clause classification using locally hosted language models.
- Semantic search across contract sections.
- Automated redaction of personal identifiers.
- Version comparison between contract drafts.
- A review interface with source-page references.
- Evaluation datasets measuring precision and recall.
A useful next step would be building a benchmark containing synthetic contracts and manually annotated clauses.
That would allow developers to measure whether more sophisticated models actually outperform simple keyword matching.
Conclusion
Legal document automation is not just an AI problem.
It combines document processing, privacy engineering, information retrieval, software security, and domain expertise.
Starting with a small, transparent, locally executed prototype makes it easier to understand the limitations before introducing more complex AI systems.
The most useful legal technology does not simply generate answers. It helps professionals locate relevant information, verify evidence, and make better-informed decisions.
Disclaimer: This tutorial is for software development and educational purposes. It does not provide legal advice or determine the validity of contractual provisions.
Top comments (1)
Outstanding write-up, Rodolfo! In a legaltech landscape where the default reaction is often "just pipe the whole 80-page contract into a commercial LLM API and pray to the DPO", advocating for a local, privacy-by-design baseline under the LGPD is both refreshing and pragmatic.
If you are planning to take this prototype further, here are three constructive architectural refinements:
1. Upgrading CPF Detection: Pattern Matching vs. Modulo-11 Validation
Your regex isolates format patterns cleanly, but real-world contracts often feature dummy values (
111.111.111-11), invoice protocol IDs, or Brazilian zip codes (CEPs) masquerading as 11-digit strings. Incorporating the official Mod-11 checksum validation eliminates false positives before triggering unnecessary human review flags:(If you ever integrate local engines like Microsoft Presidio, wrapping this in a custom
PatternRecognizerwith contextual words like"inscrito no CPF"or"representante"significantly reduces noise).2. Multi-Column Signature Blocks & Layout Traps
As you rightly noted,
page.get_text()flattens text streams naively. Brazilian contracts routinely terminate with parallel signature blocks (Contratante on the left, Contratada / Interveniente Anuente on the right). Standard sequential extraction interleaves those lines, creating disjointed clauses. Sorting coordinate blocks viapage.get_text("blocks", sort=True)or leveraging layout-aware parsers (likepymupdf4llm) will save your clause segmentation from unexpected fragmentation.3. Local Execution ≠ Automatic Privacy (The Endpoint Reality)
Cutting off external network calls eliminates cloud egress, but true LGPD compliance on-premise requires guarding the rest of the surface:
%TEMP%,/tmp) created during PDF buffer processing are explicitly sanitized.HF_HUB_DISABLE_TELEMETRY=1if integrating local transformers.4. Beyond Keywords: Where Small Specialized Models Actually Fit
Keyword matching is a solid baseline, but Brazilian legal drafting loves circumlocution—a termination clause might never mention "rescisão", relying instead on "denúncia motivada por resilição unilateral".
When exploring local ML alternatives, it is worth distinguishing between general reasoning and domain classification. Recent benchmarks on Brazilian legal text (such as LegalBench-BR, arXiv:2604.18878) showed that fine-tuned encoders like
BERTimbau-LoRAcan outperform off-the-shelf lightweight models like GPT-4o mini on specialized taxonomy tasks, largely because zero-shot models heavily over-index on civil law labels. However, for nuanced clause interpretation, a small local encoder combined with your rule-based guardrails offers a predictable, low-latency sweet spot.After all, the only thing more stressful than a hidden indemnity cap on page 42 is having your security auditor discover that your "offline" prototype left unencrypted contract chunks in the application crash dumps!
Great tutorial—looking forward to seeing the next iteration! 🚀