Here's one way you could do it.  The function get_shape(a) is a recursive function that returns either the shape of a as a tuple, or None if a does not have a regular shape (i.e. a is ragged).  The tricky part is actually the function is_scalar(a): we want string and bytes instances, and arbitrary noniterable objects (such as None, or Foo() where Foo is class Foo: pass) to be considered scalars.  np.iscalar() does some of the work; an attempt to evaluate len(a) does the rest.  The NumPy docs suggest the simple expression np.ndim(a) == 0, but that will invoke the array creation for a, which will trigger the warning if a is ragged. (The is_scalar function might miss some cases, so test it carefully with typical data that you use.)
import numpy as np
def is_scalar(a):
    if np.isscalar(a):
        return True
    try:
        len(a)
    except TypeError:
        return True
    return False
def get_shape(a):
    """
    Returns the shape of `a`, if `a` has a regular array-like shape.
    Otherwise returns None.
    """
    if is_scalar(a):
        return ()
    shapes = [get_shape(item) for item in a]
    if len(shapes) == 0:
        return (0,)
    if any([shape is None for shape in shapes]):
        return None
    if not all([shapes[0] == shape for shape in shapes[1:]]):
        return None
    return (len(shapes),) + shapes[0]
def is_ragged(a):
    return get_shape(a) is None
For example,
In [114]: is_ragged(123)
Out[114]: False
In [115]: is_ragged([1, 2, 3])
Out[115]: False
In [116]: is_ragged([1, 2, [3, 4]])
Out[116]: True
In [117]: is_ragged([[[1]], [[2]], [[3]]])
Out[117]: False
In [118]: is_ragged([[[1]], [[2]], [[3, 99]]])
Out[118]: True