Number uncovering with given equations
Functions
Split your code into individual smaller functions, doing singular tasks. A few examples would be validating the expression, extracting variables and so on.
if __name__ block
For scripts, it is a good practice to put your executable feature inside the if __name__ == &quot;__main__&quot; clause.
Comments
The comments like in:
while len(chars) &lt; 10: # If there are less that 10 different mystery numbers

are pointless. The condition already explains what you write in 10 words. A more important comment here could've been what the mystery numbers are?
This applies to almost all of the comments you have added in your program. Once you split to functions, add docstrings to those functions explaining what and maybe why the function does. The code should be self-explanatory on the how.
Execution logic
From what I understand of the program, you are:

asking user for expression
validating if there are any invalid characters
extracting variables (and creating a forced list of 10 elements)
Not sure about the findall('\d+', exp) validation, and later the same thing is being achieved by a different regex pattern
check if the expression is mathematically consistent
permute all elements from variables list (created above)
iterate on all permutations and replace into the expression
evaluate expression and print if successful

A lot of the code seems duplicated, for eg. the exp.replace calls, the findall validation etc. You have to work with 2 nested loops just because there are no helper functions to assist you.
For the permutations themselves, you can obviously permute over a forced set of length 10, or you can just permute over the integers range(10) selecting as many elements as there are variables. Since you anyway need those integers (using enumerate), I'd go for this.
As a side note, instead of printing all valid results in the main loop, use a generator and yield valid results.

Example rewrite
from typing import Tuple, Set
from itertools import count, permutations

OPERATORS = '+-*/( ).='
VARIABLES = 'abcdefghijklmnopqrstuvwxyz'
NUMERALS = '1234567890'
VALID_CHARACTERS = OPERATORS + VARIABLES + NUMERALS


def is_expression_valid(expression: str) -&gt; bool:
    return all(c in VALID_CHARACTERS for c in expression)


def is_expression_consistent(expression: str, variables: Set[str]) -&gt; bool:
    for index, value in zip(count(1), variables):
        expression = expression.replace(value, str(index))
    try:
        eval(expression.replace(&quot;=&quot;, &quot;==&quot;))
    except SyntaxError:
        return False
    return True


def extract_variables(expression: str) -&gt; Set[str]:
    variables = {char for char in expression if char in VARIABLES}
    if len(variables) &gt; 10:
        raise ValueError()
    return variables


def get_expression() -&gt; Tuple[str, Set[str]]:
    while True:
        expression = input(&quot;&gt;&gt;&gt; &quot;)
        if not is_expression_valid(expression):
            print(f&quot;Invalid characters in the expression. Use only from: {VALID_CHARACTERS}&quot;)
            continue
        try:
            variables = extract_variables(expression)
        except ValueError:
            print(&quot;Expected at most 9 variable/letters!&quot;)
            continue
        if not is_expression_consistent(expression, variables):
            print(&quot;Invalid syntax!&quot;)
            continue
        break
    return expression, variables


def substitute_expression_format(expression: str) -&gt; str:
    return &quot;&quot;.join(r&quot;{}&quot; if c in VARIABLES else c for c in expression)


def valid_results(expression: str, variables: Set[str]) -&gt; str:
    formatted_expression = substitute_expression_format(expression)
    for permute in permutations(range(10), len(variables)):
        variation = formatted_expression.format(*permute)
        if eval(variation.replace(&quot;=&quot;, &quot;==&quot;)):
            yield variation


def main():
    expression, variables = get_expression()
    for result in valid_results(expression, variables):
        print(result)


if __name__ == &quot;__main__&quot;:
    main()

Notice in is_expression_consistent, I've use zip with itertools.count, so that the dependency on regex is removed.
Now, the above fails for invalid token check, when the first substitution is \$ 0 \$; which can be resolved by having a try-except in the generator:
def valid_results(expression: str, variables: Set[str]) -&gt; str:
    formatted_expression = substitute_expression_format(expression)
    for permute in permutations(range(10), len(variables)):
        variation = formatted_expression.format(*permute)
        try:
            if eval(variation.replace(&quot;=&quot;, &quot;==&quot;)):
                yield variation
        except SyntaxError:
            continue

You can check the code on tio.run.

EDIT
For fixing the issue mentioned in comments (about repeated letters):
def substitute_expression_format(expression: str) -&gt; str:
    return &quot;&quot;.join(f&quot;{{{c}}}&quot; if c in VARIABLES else c for c in expression)


def valid_results(expression: str, variables: Set[str]) -&gt; str:
    formatted_expression = substitute_expression_format(expression)
    for permute in permutations(range(10), len(variables)):
        variation = formatted_expression.format(**dict(zip(variables, permute)))
        try:
            if eval(variation.replace(&quot;=&quot;, &quot;==&quot;)):
                yield variation
        except SyntaxError:
            continue

Updated example on tio.run.