def safeformat(format_string, *args):
    """String formatter with type validation. Python `format`-alike.
    
    Examples::
        
        - implicit indexing:
        
            safeformat("Decimal {:int}, hexadecimal {1:hex}", 2, "abc")
            --> "Decimal 2, hexadecimal abc"
    
        - reusable arguments:
        
            safeformat("{:hex}. item: {0:str}", 13)
            --> "13. item: 13"
        
        - safe paths:
        
            safeformat("objects/{:int}/tokens/{:hex}", 123, "ab2e36d953e7b634")
            --> "objects/123/tokens/ab2e36d953e7b634"
    
    Format pattern is `{[index]:type}`, where `index` is optional argument
    index, and `type` is one of the predefined typenames (currently: `int`,
    `hex`, `str`, `ident`, `iso`, `isobasic`).
    """
    
    def hex(x):
        """Allow hexadecimal digits."""
        if re.match('^[a-fA-F\d]*$', str(x)):
            return str(x)
        raise ValueError("Non-hex digits in hex number.")
    
    def identifier(x):
        """Allow letters, digits, underscore and minus/dash."""
        if re.match('^[-\w]*$', str(x)):
            return str(x)
        raise ValueError("Non-identifier characters in string.")
    
    def iso(x):
        if not isinstance(x, datetime):
            raise ValueError("Datetime expected.")
        return x.isoformat()
    
    def isobasic(x):
        if not isinstance(x, datetime):
            raise ValueError("Datetime expected.")
        return x.strftime("%Y%m%dT%H%M%S%z")
    
    converters = {
        'int': int,
        'hex': hex,
        'str': str,
        'ident': identifier,
        'iso': iso,
        'isobasic': isobasic
    }
    
    argidx = count(0)
    
    def replace(match):
        idx, typ = match.group('idx', 'typ')
        
        if idx is None:
            idx = next(argidx)
        else:
            try:
                idx = int(idx)
            except:
                raise ConversionError("Non-integer index: '%s'." % idx)
        
        try:
            arg = args[idx]
        except:
            raise ConversionError("Index out of bounds: %d." % idx)
        
        try:
            conv = converters[typ]
        except:
            raise ConversionError("Invalid converter/type: '%s'." % typ)
        
        try:
            val = conv(arg)
        except:
            raise ConversionError("Argument '%s' not of type '%s'." % (arg, typ))
        
        return str(val)
    
    return re.sub("{(?:(?P<idx>\d+))?:(?P<typ>\w+)}", replace, format_string)