DEV Community

Rodolfo Santos
Rodolfo Santos

Posted on

Building a Privacy-First Contract Analyzer in Python for Brazilian Legal Documents

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"
    ]
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

For example:

sample = """
Contratante: Maria Silva
CPF: 123.456.789-09
"""

print(find_potential_cpfs(sample))
Enter fullscreen mode Exit fullscreen mode

Output:

['123.456.789-09']
Enter fullscreen mode Exit fullscreen mode

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
        )
    )
Enter fullscreen mode Exit fullscreen mode

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:

  1. Clause classification using locally hosted language models.
  2. Semantic search across contract sections.
  3. Automated redaction of personal identifiers.
  4. Version comparison between contract drafts.
  5. A review interface with source-page references.
  6. 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)

Collapse
 
fgrandon profile image
Francisco Grandón •

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:

def is_valid_cpf(cpf_str: str) -> bool:
    digits = [int(d) for d in cpf_str if d.isdigit()]
    if len(digits) != 11 or len(set(digits)) == 1:
        return False
    for i in (9, 10):
        expected = sum(digits[j] * ((i + 1) - j) for j in range(i)) % 11
        expected = 0 if expected < 2 else 11 - expected
        if digits[i] != expected:
            return False
    return True
Enter fullscreen mode Exit fullscreen mode

(If you ever integrate local engines like Microsoft Presidio, wrapping this in a custom PatternRecognizer with 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 via page.get_text("blocks", sort=True) or leveraging layout-aware parsers (like pymupdf4llm) 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:

  • Temporary files & swap: Ensuring OS temp caches (%TEMP%, /tmp) created during PDF buffer processing are explicitly sanitized.
  • Log sanitation: Preventing debug loggers from dumping unmasked contract snippets into plaintext disk logs.
  • Telemetry suppression: Setting flags like HF_HUB_DISABLE_TELEMETRY=1 if 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-LoRA can 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! 🚀

⌂ 换网址