A really nice feature of cocotb is force and release which map to RTL force and release depending on the language (and simulator). This is example how to use Force and Release.

from cocotb.handle import Force, Release, Deposit
from cocotb.binary import BinaryValue

value = "0"
sig  = ....

sig.value = Force(BinaryValue(len(sig) * value)
sig.value = Release()

Force and Release are defines in handle.py and defines _as_gpi_args_for.

class _SetValueAction(_SetAction):
    __slots__ = ("value",)
    """Base class representing the type of action used while write-accessing a handle with a value."""

    def __init__(self, value):
        self.value = value


class Force(_SetValueAction):
    """Action used to force a handle to a given value until a release is applied."""

    def _as_gpi_args_for(self, hdl):
        return self.value, 1  # GPI_FORCE

class Release(_SetAction):
    """Action used to stop the effects of a previously applied force/freeze action."""

    def _as_gpi_args_for(self, hdl):
        return 0, 2  # GPI_RELEASE

Ok, Let’s look what happens when testbench calls sig.value = Force(v). Starting with setter function which calls _set_value.

    @value.setter
    def value(self, value):
        self._set_value(value, cocotb.scheduler._schedule_write)

For integer, IntegerObject calls _check_for_set_action

class IntegerObject(ModifiableObject):

        value, set_action = self._check_for_set_action(value)
    def _set_value(self, value, call_sim):
        ...


         call_sim(self, self._handle.set_signal_val_int, set_action, value)

And _check_for_set_action calls _as_gpi_args_for to get type of action and pass it down to simulator.


    def _check_for_set_action(self, value):
        if not isinstance(value, _SetAction):
            return value, 0  # GPI_DEPOSIT
        return value._as_gpi_args_for(self)

well, Several layers(gpi and stuff), there are several implementations of force(FLI/VHPI and VPI). This is snippet from the vpi set_signal_value. vpiForceFlag is passed to vpi_put_value.

int VpiSignalObjHdl::set_signal_value(s_vpi_value value_s,
                                      gpi_set_action_t action) {
    PLI_INT32 vpi_put_flag = -1;
    s_vpi_time vpi_time_s;

    vpi_time_s.type = vpiSimTime;
    vpi_time_s.high = 0;
    vpi_time_s.low = 0;

    switch (action) {
        case GPI_DEPOSIT:
            if (vpiStringVar ==
                vpi_get(vpiType, GpiObjHdl::get_handle<vpiHandle>())) {
                // assigning to a vpiStringVar only seems to work with
                // vpiNoDelay
                vpi_put_flag = vpiNoDelay;
            } else {
                // Use Inertial delay to schedule an event, thus behaving like a
                // verilog testbench
                vpi_put_flag = vpiInertialDelay;
            }
            break;
        case GPI_FORCE:
            vpi_put_flag = vpiForceFlag;
            break;
        case GPI_RELEASE:
            // Best to pass its current value to the sim when releasing
            vpi_get_value(GpiObjHdl::get_handle<vpiHandle>(), &value_s);
            vpi_put_flag = vpiReleaseFlag;
            break;
        default:
            assert(0);
    }

    if (vpi_put_flag == vpiNoDelay) {
        vpi_put_value(GpiObjHdl::get_handle<vpiHandle>(), &value_s, NULL,
                      vpiNoDelay);
    } else {
        vpi_put_value(GpiObjHdl::get_handle<vpiHandle>(), &value_s, &vpi_time_s,
                      vpi_put_flag);
    }