High severity intermediate · Fix: 5-10 min

XMLParseError / ValueError

xml.etree.ElementTree.ParseError or ValueError: malformed thinking tag structure

What this error means
DeepSeek-R1's response includes <thinking> tags that don't parse as valid XML, causing extraction and parsing failures in reasoning chains.

Stack trace

traceback
Traceback (most recent call last):
  File "chain.py", line 42, in extract_thinking
    root = ET.fromstring(thinking_block)
xml.etree.ElementTree.ParseError: syntax error: line 1, column 0

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "main.py", line 15, in <module>
    result = client.chat.completions.create(
ValueError: Could not parse thinking tags from response. Expected format: <thinking>...</thinking>, got malformed XML.
QUICK FIX
Strip markdown fences and use regex extraction instead of XML parsing: `thinking = re.search(r'<thinking>(.*?)</thinking>', response, re.DOTALL).group(1) if re.search(r'<thinking>(.*?)</thinking>', response, re.DOTALL) else ''`

Why it happens

DeepSeek-R1 returns reasoning in <thinking>...</thinking> XML tags, but the model sometimes nests unclosed tags, includes special characters that break XML parsers, or returns thinking blocks wrapped in markdown fences (```xml). When your code tries to extract and parse these tags with xml.etree.ElementTree or regex, the parser fails because the structure violates XML well-formedness rules. This is particularly common when the model interrupts reasoning or generates edge-case token sequences.

Detection

Log the raw API response before parsing thinking tags. Check if thinking_block contains unescaped &, <, > characters, unclosed tags, or markdown wrappers. Add a validation step: `if not thinking_block.strip().startswith('<thinking>')` before parsing.

Causes & fixes

1

Thinking block contains unescaped special XML characters (&, <, >, ", ') that break the parser

✓ Fix

Use html.unescape() and re.escape() before parsing, or use a lenient XML parser: `xml.dom.minidom.parseString(thinking_block)` with error handling that skips malformed blocks

2

Thinking tags are wrapped in markdown code fences (```xml <thinking>...) instead of raw XML

✓ Fix

Strip markdown fences before parsing: `thinking_block = re.sub(r'^```\w*\n|\n```$', '', thinking_block).strip()` then parse the cleaned string

3

Nested or unclosed <thinking> tags, or missing closing tag due to token limit truncation

✓ Fix

Implement fallback parsing with regex instead of XML: `match = re.search(r'<thinking>(.*?)</thinking>', response, re.DOTALL)` or use BeautifulSoup with `features='html.parser'` which is more forgiving

4

Using old DeepSeek SDK (v0.x) that doesn't properly format reasoning output

✓ Fix

Upgrade to latest DeepSeek SDK: `pip install --upgrade deepseek-python-sdk` or use OpenAI-compatible endpoint with `client = OpenAI(api_key=..., base_url='https://api.deepseek.com/v1')`

Code: broken vs fixed

Broken - triggers the error
python
import os
import xml.etree.ElementTree as ET
from openai import OpenAI

client = OpenAI(
    api_key=os.environ.get('DEEPSEEK_API_KEY'),
    base_url='https://api.deepseek.com/v1'
)

response = client.chat.completions.create(
    model='deepseek-reasoner',
    messages=[{'role': 'user', 'content': 'Solve: 5x + 3 = 28'}],
    max_completion_tokens=16000
)

# THIS BREAKS: Assumes thinking tags are always valid XML
full_response = response.choices[0].message.content
thinking_match = full_response.find('<thinking>')
if thinking_match != -1:
    end_match = full_response.find('</thinking>') + len('</thinking>')
    thinking_block = full_response[thinking_match:end_match]
    # ERROR: thinking_block may contain unescaped chars or markdown fences
    root = ET.fromstring(thinking_block)  # <- CRASHES HERE with ParseError
    print(f'Thinking: {root.text}')
Fixed - works correctly
python
import os
import re
from openai import OpenAI

client = OpenAI(
    api_key=os.environ.get('DEEPSEEK_API_KEY'),
    base_url='https://api.deepseek.com/v1'
)

response = client.chat.completions.create(
    model='deepseek-reasoner',
    messages=[{'role': 'user', 'content': 'Solve: 5x + 3 = 28'}],
    max_completion_tokens=16000
)

full_response = response.choices[0].message.content

# FIXED: Use regex extraction with markdown fence stripping
# This handles malformed XML, unclosed tags, and markdown wrappers gracefully
thinking_match = re.search(r'<thinking>(.*?)</thinking>', full_response, re.DOTALL)
if thinking_match:
    thinking_text = thinking_match.group(1).strip()
    # Remove any markdown fences that might wrap the thinking block
    thinking_text = re.sub(r'^```\w*\n|\n```$', '', thinking_text).strip()
    print(f'Thinking (extracted): {thinking_text}')
else:
    print('No valid thinking tags found in response')

# Extract final answer after thinking
answer_match = re.search(r'(?:</thinking>)\s*(.*?)$', full_response, re.DOTALL)
if answer_match:
    print(f'Answer: {answer_match.group(1).strip()}')
Switched from strict XML parsing (ET.fromstring) to regex extraction with markdown fence stripping, which handles malformed tags, unescaped characters, and truncation gracefully without crashing.
⚠

Workaround

When regex extraction fails, fall back to splitting on closing tag: `parts = response.split('</thinking>'); thinking = parts[0].replace('<thinking>', '').strip() if len(parts) > 0 else ''; answer = parts[1].strip() if len(parts) > 1 else ''`. This handles edge cases where tags are present but malformed, extracting whatever reasoning content exists.

✓

Prevention

Always extract reasoning blocks using lenient regex (re.DOTALL flag for multiline) rather than strict XML parsing. Validate that the response actually contains <thinking> tags before attempting extraction. Log the raw response before and after cleaning for debugging. Use the OpenAI-compatible DeepSeek endpoint (api.deepseek.com/v1) instead of native SDK, which has more stable formatting. Add a fallback to Claude (claude-opus-4) for reasoning tasks if DeepSeek response parsing fails repeatedly in production.

Python 3.9+ · openai >=1.3.0 · tested on 1.40.0
Verified 2026-04 · deepseek-reasoner (R1)
Verify ↗

Community Notes

No notes yetBe the first to share a version-specific fix or tip.