Can the χ² test be used without a contingency table? I thought that the chi squared (χ²) test is to be used when one has an r × c contingency matrix, i.e., when the dependent variables are nonnegative, span the same r dimension, and typically represent a count, frequency, or boolean value (cf. for example, p. 124 of Practical Statistics for Data Scientists by Bruce and Bruce).
I'm therefore confused by this scikit-learn tutorial which uses the χ² test in ranking the features of the Iris dataset. Aren't those feature continuous? Where then is the rectangular contingency matrix from which the Pearson residuals, and hence the chi-squared value are derived?
PS: Here, I'd consider the c flower categories as the independent variables and the four features are the dependent variables. Would that be accurate?

There are other chi-squared tests, but the numpy function chi2 performs the Pearson's chi-squared test for contingency tables.
The Pearson's chi-squared test computes expected and observed frequencies and then passes these to a function that computes a chi-squared statistic with the formula
$$\chi^2 = \sum_{\forall i} \frac{(O_i - E_i)^2}{E_i}$$
This formula is specific for counts data. This should not be used for continuous variables (and neither for frequencies mentioned in the source code comments and in the manual, frequencies are fractions of counts).
The reason that you must use counts is because the statistic is based on a specific relationship between the mean and the variance of the counts for data that follows a multinomial distribution. With other type of data than counts, the relationship between the mean and variance can be completely different.
def _chisquare(f_obs, f_exp):
    """Fast replacement for scipy.stats.chisquare.
    Version from https://github.com/scipy/scipy/pull/2525 with additional
    optimizations.
    """
    f_obs = np.asarray(f_obs, dtype=np.float64)

    k = len(f_obs)
    # Reuse f_obs for chi-squared statistics
    chisq = f_obs
    chisq -= f_exp
    chisq **= 2
    with np.errstate(invalid="ignore"):
        chisq /= f_exp
    chisq = chisq.sum(axis=0)
    return chisq, special.chdtrc(k - 1, chisq)


def chi2(X, y):
    """Compute chi-squared stats between each non-negative feature and class.
    This score can be used to select the n_features features with the
    highest values for the test chi-squared statistic from X, which must
    contain only non-negative features such as booleans or frequencies
    (e.g., term counts in document classification), relative to the classes.
    Recall that the chi-square test measures dependence between stochastic
    variables, so using this function "weeds out" the features that are the
    most likely to be independent of class and therefore irrelevant for
    classification.
    Read more in the :ref:`User Guide <univariate_feature_selection>`.
    Parameters
    ----------
    X : {array-like, sparse matrix}, shape = (n_samples, n_features_in)
        Sample vectors.
    y : array-like, shape = (n_samples,)
        Target vector (class labels).
    Returns
    -------
    chi2 : array, shape = (n_features,)
        chi2 statistics of each feature.
    pval : array, shape = (n_features,)
        p-values of each feature.
    Notes
    -----
    Complexity of this algorithm is O(n_classes * n_features).
    See also
    --------
    f_classif: ANOVA F-value between label/feature for classification tasks.
    f_regression: F-value between label/feature for regression tasks.
    """

    # XXX: we might want to do some of the following in logspace instead for
    # numerical stability.
    X = check_array(X, accept_sparse='csr')
    if np.any((X.data if issparse(X) else X) < 0):
        raise ValueError("Input X must be non-negative.")

    Y = LabelBinarizer().fit_transform(y)
    if Y.shape[1] == 1:
        Y = np.append(1 - Y, Y, axis=1)

    observed = safe_sparse_dot(Y.T, X)          # n_classes * n_features

    feature_count = X.sum(axis=0).reshape(1, -1)
    class_prob = Y.mean(axis=0).reshape(1, -1)
    expected = np.dot(class_prob.T, feature_count)

    return _chisquare(observed, expected)

