Из-за разной длины текста они выходят разной высоты.
Не выходят. Это страсть к лишним оберткам вводит вас в заблуждение.
Карточки у вас уже одной высоты. Потому что по умолчанию align-items имеет значение stretch, которое растягивает все дочерние флекс элементы на одну высоту в рамках строки. Обратите внимание на последний нюанс.
Чтобы вообще все карточки были одинаковой высоты, нужен либо грид либо js. Но обычно этого и не требуется.
Если вы обведете все practice-column рамочками, то убедитесь в этом.
А дальше все дочерние обертки растягиваете по высоте на высоту родителя. Последнему задаете флекс и раздвигаете его два дочерних элемента либо с помощью margin-top auto либо space-between.
Но всё это можно еще упростить, если ответить на вопрос, зачем вам столько оберток? Зачем нужен practice-column, если вся карточка обернута в ссылку? Зачем внутри ссылки ещё один div practice-item? Эта обертка practice-item-descr тоже не ясно с какой целью понадобилась. Но может там на макете какой-то фон отдельный.. Макета вы не показали.
Имеет смысл поступить так:
1. practice-body сделать списком, а не div.
2. practice-column - будет li. Все они окажутся одинаковой высоты в рамках строки.
3. Ссылку растянуть на всю высоту li и задать flex и ему column (хотя я бы предпочла гриды).
4. practice-item - убрать. Я не придумала, зачем он там может понадобиться.
5. Если в макете нет оснований для practice-item-descr, то тоже убрать.
6. А вот картинку имеет смысл оборачивать в div или figure, чтобы при адаптиве ничего никуда не уползло, для сохранения пропорций и т.д. (но это на ваше усмотрение)
7. Останется ссылка-флекс и в ней 3 дочерних элемента. Последнему задаете margin-top: auto.
Проблемы будут если захочется выравнивать и заголовок и текст. Тут два варианта: либо к тому времени когда захочется подтянется subgrid либо js.