Grow vertex group - withnout ops
My take on this.

Sometimes it's more trouble than it's worth to completely avoid operators. Given the nature of this method I can't imagine you will be running it in a loop hundreds of times.  Adding a modifier I would always use the API method. Selecting and assigning vertex groups using the operator is often the go, but you are constricted by the need to be in a certain mode for context.

Here's a method to do same, but with no operators. Can get the object from the vertex group, so that's all I pass to the method with: the number of times to grow the vertex group; and the name of the mask modifier so we don't end up adding modifiers endlessly to the stack.

I'm using a bmesh, for the ease of finding the linked faces for each vertex.

Test code, run in object mode with a mesh object having a vertex group,  as context object.

import bpy
import bmesh

def mask_and_grow(vg, num=1, modname="Mask"):
    ob = vg.id_data
    mod = ob.modifiers.get(modname) or ob.modifiers.new(modname, type='MASK')
    mod.vertex_group = vg.name
    me = ob.data
    bm = bmesh.new()
    bm.from_mesh(me)
    bm.verts.ensure_lookup_table()
    def ingroup(vg, v):
        vert = me.vertices[v.index] # use either bm or me
        return vg.index in [vg.group for vg in vert.groups] 

    def grow(v):
        return (v for f in v.link_faces
                for v in f.verts
                if not ingroup(vg, v))

    vg_verts =  (bm.verts[v.index] for v in me.vertices 
            if ingroup(vg, v))    
    for i in range(num):
        # only need to look at extended next time
        vg_verts = list(set(v for g in [grow(vgv) for vgv in vg_verts] for v in g))
        vg.add([v.index for v in vg_verts],
                1.0,
                'REPLACE')  
    #me.update()
    bm.free()

# test call
ob = bpy.context.object
vg = ob.vertex_groups.active
mask_and_grow(vg, num=2)