Sam Hooke

Python click: allow user to retry input upon validation failure

It can be a frustrating experience for a user if you naïvely reject their input without giving them another chance to correct their input, or know why it is wrong:

def validator(ctx, param, value):
    """Aborts if value is too long.
    """
    if len(value) > 32:
        raise click.UsageError("Too long!")
    return value

@click.command()
@click.option("--name", callback=validator)
def assign(name):
    click.echo(f"Assigning {name}")

Using the above assign command with a namer longer than 32 characters will cause the command to abort. This frustration compounds if the command takes multiple options, since the user may have already given input to several options, only to have to retry and enter it all over again.

Instead it is much better to let the user retry straightaway if their input fails:

def validator(ctx, param, value):
    """Lets user retry, with explanation, if value is too long.
    """
    while True:
        if len(value) > 32:
            # Actually tell the user why their input was rejected
            err = "must be 32 characters or fewer"
        else:
            return value
        
        # `get_error_hint` will return `"--name-"`
        hint = param.get_error_hint(ctx)
        
        # Use styling to make the error message prominent
        click.secho(f"Error: Invalid value for {hint}: {err}", fg="red", bold=True)
        
        # Prompt the user to try again
        value = click.prompt(param.prompt)

@click.command()
@click.option("--name", callback=validator)
def assign(name):
    click.echo(f"Assigning {name}")

The above implementation is better because:

  • The user can try again any number of times (they can CTRL-C to abort)
  • The user is told why their input was rejected
  • (As a bonus) the user can easily distinguish at a glance that it is an error because of the styling

These are rough notes that vary greatly in quality and length, but prove useful to me, and hopefully to you too!

← Previous: Call Python script from pylint init-hook
Next: Configure Python package to install dependencies only for specific combinations of Python version and platform →